8
Gaining Proficiency with Classes and Objects

INTRODUCING THE SPREADSHEET EXAMPLE

Both this chapter and the next present a runnable example of a simple spreadsheet application. A spreadsheet is a two-dimensional grid of “cells,” and each cell contains a number or a string. Professional spreadsheets such as Microsoft Excel provide the ability to perform mathematical operations, such as calculating the sum of the values of a set of cells. The spreadsheet example in these chapters does not attempt to challenge Microsoft in the marketplace, but is useful for illustrating the issues of classes and objects.

The spreadsheet application uses two basic classes: Spreadsheet and SpreadsheetCell. Each Spreadsheet object contains SpreadsheetCell objects. In addition, a SpreadsheetApplication class manages a collection of Spreadsheets. This chapter focuses on the SpreadsheetCell. Chapter 9 develops the Spreadsheet and SpreadsheetApplication classes.

WRITING CLASSES

When you write a class, you specify the behaviors, or methods, that will apply to objects of that class and the properties, or data members, that each object will contain.

There are two components in the process of writing classes: defining the classes themselves and defining their methods.

Class Definitions

Here is a first attempt at a simple SpreadsheetCell class, in which each cell can store only a single number:

class SpreadsheetCell
{
    public:
        void setValue(double inValue);
        double getValue() const;
    private:
        double mValue;
};

As described in Chapter 1, every class definition begins with the keyword class and the name of the class. A class definition is a statement in C++, so it must end with a semicolon. If you fail to terminate your class definition with a semicolon, your compiler will probably give you several errors, most of which will appear to be completely unrelated.

Class definitions usually go in a file named after the class. For example, the SpreadsheetCell class definition can be put in a file called SpreadsheetCell.h. This rule is not enforced and you are free to name your file whatever you like.

Class Members

A class can have a number of members. A member can be a member function (which in turn is a method, constructor, or destructor), a member variable, also called a data member, member enumerations, type aliases, nested classes, and so on.

The two lines that look like function prototypes declare the methods that this class supports:

void setValue(double inValue);
double getValue() const;

Chapter 1 points out that it is always a good idea to declare member functions that do not change the object as const.

The line that looks like a variable declaration declares the data member for this class.

double mValue;

A class defines the member functions and data members that apply. They apply only to a specific instance of the class, which is an object. The only exceptions to this rule are static members, which are explained in Chapter 9. Classes define concepts; objects contain real bits. So, each object contains its own value for the mValue variable. The implementation of the member functions is shared across all objects. Classes can contain any number of member functions and data members. You cannot give a data member the same name as a member function.

Access Control

Every member in a class is subject to one of three access specifiers: public, protected, or private. An access specifier applies to all member declarations that follow it, until the next access specifier. In the SpreadsheetCell class, the setValue() and getValue() methods have public access, while the mValue data member has private access.

The default access specifier for classes is private: all member declarations before the first access specifier have the private access specification. For example, moving the public access specifier below the setValue() method declaration gives the setValue() method private access instead of public:

class SpreadsheetCell
{
        void setValue(double inValue); // now has private access
    public:
        double getValue() const;
    private:
        double mValue;
};

In C++, a struct can have methods just like a class. In fact, the only difference is that the default access specifier for a struct is public while the default for a class is private. For example, the SpreadsheetCell class can be rewritten using a struct as follows:

struct SpreadsheetCell
{
        void setValue(double inValue);
        double getValue() const;
    private:
        double mValue;
};

It’s custom to use a struct instead of a class if you only need a collection of publicly accessible data members and no or very few methods. An example of such a simple struct is a structure to store point coordinates:

struct Point
{
    double x;
    double y;
};

The following table summarizes the meanings of the three access specifiers:

ACCESS SPECIFICATION MEANING WHEN TO USE
public Any code can call a public member function or access a public data member of an object. Behaviors (methods) that you want clients to use
Access methods (getters and setters) for private and protected data members
protected Any member function of the class can call protected member functions and access protected data members. Member functions of a derived class can access protected members of a base class. “Helper” methods that you do not want clients to use
private Only member functions of the class can call private member functions and access private data members. Member functions in derived classes cannot access private members from a base class. Everything should be private by default, especially data members. You can provide protected getters and setters if you only want to allow derived classes to access them, and provide public getters and setters if you want clients to access them.

Order of Declarations

You can declare your members and access control specifiers in any order: C++ does not impose any restrictions, such as member functions before data members or public before private. Additionally, you can repeat access specifiers. For example, the SpreadsheetCell definition could look like this:

class SpreadsheetCell
{
    public:
        void setValue(double inValue);
    private:
        double mValue;
    public:
        double getValue() const;
};

However, for clarity it is a good idea to group public, protected, and private declarations, and to group member functions and data members within those declarations.

In-Class Member Initializers

Member variables can be initialized directly in the class definition. For example, the SpreadsheetCell class can, by default, initialize mValue to 0 directly in the class definition as follows:

class SpreadsheetCell
{
    // Remainder of the class definition omitted for brevity
private:
    double mValue = 0;
};

Defining Methods

The preceding definition for the SpreadsheetCell class is enough for you to create objects of the class. However, if you try to call the setValue() or getValue() methods, your linker will complain that those methods are not defined. That’s because the class definition specifies the prototypes for the methods, but does not define their implementations. Just as you write both a prototype and a definition for a stand-alone function, you must write a prototype and a definition for a method. Note that the class definition must precede the method definitions. Usually the class definition goes in a header file, and the method definitions go in a source file that #includes that header file. Here are the definitions for the two methods of the SpreadsheetCell class:

#include "SpreadsheetCell.h"

void SpreadsheetCell::setValue(double inValue)
{
    mValue = inValue;
}

double SpreadsheetCell::getValue() const
{
    return mValue;
}

Note that the name of the class followed by two colons precedes each method name:

void SpreadsheetCell::setValue(double inValue)

The :: is called the scope resolution operator. In this context, the syntax tells the compiler that the coming definition of the setValue() method is part of the SpreadsheetCell class. Note also that you do not repeat the access specification when you define the method.

Accessing Data Members

Non-static methods of a class, such as setValue() and getValue(), are always executed on behalf of a specific object of that class. Inside a method body, you have access to all data members of the class for that object. In the previous definition for setValue(), the following line changes the mValue variable inside whatever object calls the method:

mValue = inValue;

If setValue() is called for two different objects, the same line of code (executed once for each object) changes the variable in two different objects.

Calling Other Methods

You can call methods of a class from inside another method. For example, consider an extension to the SpreadsheetCell class. Real spreadsheet applications allow text data as well as numbers in the cells. When you try to interpret a text cell as a number, the spreadsheet tries to convert the text to a number. If the text does not represent a valid number, the cell value is ignored. In this program, strings that are not numbers will generate a cell value of 0. Here is a first stab at a class definition for a SpreadsheetCell that supports text data:

#include <string>
#include <string_view>
class SpreadsheetCell
{
    public:
        void setValue(double inValue);
        double getValue() const;

        void setString(std::string_view inString);
        std::string getString() const;
    private:
        std::string doubleToString(double inValue) const;
        double stringToDouble(std::string_view inString) const;
        double mValue;
};

This version of the class stores the data only as a double. If the client sets the data as a string, it is converted to a double. If the text is not a valid number, the double value is set to 0.0. The class definition shows two new methods to set and retrieve the text representation of the cell, and two new private helper methods to convert a double to a string and vice versa. Here are the implementations of all the methods:

#include "SpreadsheetCell.h"
using namespace std;

void SpreadsheetCell::setValue(double inValue)
{
    mValue = inValue;
}

double SpreadsheetCell::getValue() const
{
    return mValue;
}

void SpreadsheetCell::setString(string_view inString)
{
    mValue = stringToDouble(inString);
}

string SpreadsheetCell::getString() const
{
    return doubleToString(mValue);
}

string SpreadsheetCell::doubleToString(double inValue) const
{
    return to_string(inValue);
}

double SpreadsheetCell::stringToDouble(string_view inString) const
{
    return strtod(inString.data(), nullptr);
}

Note that with this implementation of the doubleToString() method, a value of, for example, 6.1 is converted to “6.100000”. However, because it is a private helper method, you are free to modify the implementation without having to modify any client code.

The this Pointer

Every normal method call passes a pointer to the object for which it is called as a “hidden” parameter with the name this. You can use this pointer to access data members or call methods, and you can pass it to other methods or functions. It is sometimes also useful for disambiguating names. For example, you could have defined the SpreadsheetCell class with a value data member instead of mValue and you could have defined the setValue() method to take a parameter named value instead of inValue. In that case, setValue() would look like this:

void SpreadsheetCell::setValue(double value)
{
    value = value; // Ambiguous!
}

That line is confusing. Which value do you mean: the value that was passed as a parameter, or the value that is a member of the object?

In order to disambiguate the names, you can use the this pointer:

void SpreadsheetCell::setValue(double value)
{
    this->value = value;
}

However, if you use the naming conventions described in Chapter 3, you will never encounter this type of name collision.

You can also use the this pointer to call a function or method that takes a pointer to an object from within a method of that object. For example, suppose you write a printCell() stand-alone function (not method) like this:

void printCell(const SpreadsheetCell& cell)
{
    cout << cell.getString() << endl;
}

If you want to call printCell() from the setValue() method, you must pass *this as the argument to give printCell() a reference to the SpreadsheetCell on which setValue() operates:

void SpreadsheetCell::setValue(double value)
{
    this->value = value;
    printCell(*this);
}

Using Objects

The previous class definition says that a SpreadsheetCell consists of one data member, four public methods, and two private methods. However, the class definition does not actually create any SpreadsheetCells; it just specifies their shape and behavior. In that sense, a class is similar to architectural blueprints. The blueprints specify what a house should look like, but drawing the blueprints doesn’t build any houses. Houses must be constructed later based on the blueprints.

Similarly, in C++ you can construct a SpreadsheetCell “object” from the SpreadsheetCell class definition by declaring a variable of type SpreadsheetCell. Just as a builder can build more than one house based on a given set of blueprints, a programmer can create more than one SpreadsheetCell object from a SpreadsheetCell class. There are two ways to create and use objects: on the stack and on the heap.

Objects on the Stack

Here is some code that creates and uses SpreadsheetCell objects on the stack:

SpreadsheetCell myCell, anotherCell;
myCell.setValue(6);
anotherCell.setString("3.2");
cout << "cell 1: " << myCell.getValue() << endl;
cout << "cell 2: " << anotherCell.getValue() << endl;

You create objects just as you declare simple variables, except that the variable type is the class name. The . in lines like myCell.setValue(6); is called the “dot” operator; it allows you to call methods on the object. If there were any public data members in the object, you could access them with the dot operator as well. Remember that public data members are not recommended.

The output of the program is as follows:

cell 1: 6
cell 2: 3.2

Objects on the Heap

You can also dynamically allocate objects by using new:

SpreadsheetCell* myCellp = new SpreadsheetCell();
myCellp->setValue(3.7);
cout << "cell 1: " << myCellp->getValue() <<
        " " << myCellp->getString() << endl;
delete myCellp;
myCellp = nullptr;

When you create an object on the heap, you access its members through the “arrow” operator: ->. The arrow combines dereferencing (*) and member access (.). You could use those two operators instead, but doing so would be stylistically awkward:

SpreadsheetCell* myCellp = new SpreadsheetCell();
(*myCellp).setValue(3.7);
cout << "cell 1: " << (*myCellp).getValue() <<
        " " << (*myCellp).getString() << endl;
delete myCellp;
myCellp = nullptr;

Just as you must free other memory that you allocate on the heap, you must free the memory for objects that you allocate on the heap by calling delete on the objects. To guarantee safety and to avoid memory problems, you should use smart pointers, as in the following example:

auto myCellp = make_unique<SpreadsheetCell>();
// Equivalent to:
// unique_ptr<SpreadsheetCell> myCellp(new SpreadsheetCell());
myCellp->setValue(3.7);
cout << "cell 1: " << myCellp->getValue() <<
        " " << myCellp->getString() << endl;

With smart pointers you don’t need to manually free the memory; it happens automatically.

OBJECT LIFE CYCLES

The object life cycle involves three activities: creation, destruction, and assignment. It is important to understand how and when objects are created, destroyed, and assigned, and how you can customize these behaviors.

Object Creation

Objects are created at the point you declare them (if they’re on the stack) or when you explicitly allocate space for them with new, new[], or a smart pointer. When an object is created, all its embedded objects are also created. Here is an example:

#include <string>

class MyClass
{
    private:
        std::string mName;
};

int main()
{
    MyClass obj;
    return 0;
}

The embedded string object is created at the point where the MyClass object is created in the main() function and is destructed when its containing object is destructed.

It is often helpful to give variables initial values as you declare them, as in this example:

int x = 0;

Similarly, you should give initial values to objects. You can provide this functionality by declaring and writing a special method called a constructor, in which you can perform initialization work for the object. Whenever an object is created, one of its constructors is executed.

Writing Constructors

Syntactically, a constructor is specified by a method name that is the same as the class name. A constructor never has a return type and may or may not have parameters. A constructor that can be called without any arguments is called a default constructor. This can be a constructor that does not have any parameters, or a constructor for which all parameters have default values. There are certain contexts in which you may have to provide a default constructor and you will get compilation errors if you have not provided one. Default constructors are discussed later in this chapter.

Here is a first attempt at adding a constructor to the SpreadsheetCell class:

class SpreadsheetCell
{
    public:
        SpreadsheetCell(double initialValue);
        // Remainder of the class definition omitted for brevity
};

Just as you must provide implementations for normal methods, you must provide an implementation for the constructor:

SpreadsheetCell::SpreadsheetCell(double initialValue)
{
    setValue(initialValue);
}

The SpreadsheetCell constructor is a member of the SpreadsheetCell class, so C++ requires the normal SpreadsheetCell:: scope resolution before the constructor name. The constructor name itself is also SpreadsheetCell, so the code ends up with the funny-looking SpreadsheetCell::SpreadsheetCell. The implementation simply makes a call to setValue().

Using Constructors

Using the constructor creates an object and initializes its values. You can use constructors with both stack-based and heap-based allocation.

Constructors on the Stack

When you allocate a SpreadsheetCell object on the stack, you use the constructor like this:

SpreadsheetCell myCell(5), anotherCell(4);
cout << "cell 1: " << myCell.getValue() << endl;
cout << "cell 2: " << anotherCell.getValue() << endl;

Note that you do not call the SpreadsheetCell constructor explicitly. For example, do not use something like the following:

SpreadsheetCell myCell.SpreadsheetCell(5); // WILL NOT COMPILE!

Similarly, you cannot call the constructor later. The following is also incorrect:

SpreadsheetCell myCell;
myCell.SpreadsheetCell(5); // WILL NOT COMPILE!

Constructors on the Heap

When you dynamically allocate a SpreadsheetCell object, you use the constructor like this:

auto smartCellp = make_unique<SpreadsheetCell>(4);
// … do something with the cell, no need to delete the smart pointer

// Or with raw pointers, without smart pointers (not recommended)
SpreadsheetCell* myCellp = new SpreadsheetCell(5);
SpreadsheetCell* anotherCellp = nullptr;
anotherCellp = new SpreadsheetCell(4);
// … do something with the cells
delete myCellp;            myCellp = nullptr;
delete anotherCellp;       anotherCellp = nullptr;

Note that you can declare a pointer to a SpreadsheetCell object without calling the constructor immediately, which is different from objects on the stack, where the constructor is called at the point of declaration.

If you declare a pointer on the stack in a function, or declare a pointer as a data member in a class, and you don’t immediately initialize the pointer, then it should be initialized to nullptr as in the previous declaration for anotherCellp. If you don’t assign it to nullptr, the pointer is undefined. Accidentally using an undefined pointer will cause unexpected and difficult-to-diagnose memory corruption. If you initialize it to nullptr, using that pointer will cause a memory access error in most operating environments, instead of producing unexpected results.

Remember to call delete on objects that you dynamically allocate with new, or use smart pointers!

Providing Multiple Constructors

You can provide more than one constructor in a class. All constructors have the same name (the name of the class), but different constructors must take a different number of arguments or different argument types. In C++, if you have more than one function with the same name, the compiler selects the one whose parameter types match the types at the call site. This is called overloading and is discussed in detail in Chapter 9.

In the SpreadsheetCell class, it is helpful to have two constructors: one to take an initial double value and one to take an initial string value. Here is the new class definition:

class SpreadsheetCell
{
    public:
        SpreadsheetCell(double initialValue);
        SpreadsheetCell(std::string_view initialValue);
        // Remainder of the class definition omitted for brevity
};

Here is the implementation of the second constructor:

SpreadsheetCell::SpreadsheetCell(string_view initialValue)
{
    setString(initialValue);
}

And here is some code that uses the two different constructors:

SpreadsheetCell aThirdCell("test");  // Uses string-arg ctor
SpreadsheetCell aFourthCell(4.4);    // Uses double-arg ctor
auto aFifthCellp = make_unique<SpreadsheetCell>("5.5"); // string-arg ctor
cout << "aThirdCell: " << aThirdCell.getValue() << endl;
cout << "aFourthCell: " << aFourthCell.getValue() << endl;
cout << "aFifthCellp: " << aFifthCellp->getValue() << endl;

When you have multiple constructors, it is tempting to try to implement one constructor in terms of another. For example, you might want to call the double constructor from the string constructor as follows:

SpreadsheetCell::SpreadsheetCell(string_view initialValue)
{
    SpreadsheetCell(stringToDouble(initialValue));
}

That seems to make sense. After all, you can call normal class methods from within other methods. The code will compile, link, and run, but will not do what you expect. The explicit call to the SpreadsheetCell constructor actually creates a new temporary unnamed object of type SpreadsheetCell. It does not call the constructor for the object that you are supposed to be initializing.

However, C++ supports delegating constructors that allow you to call other constructors from the same class from inside the ctor-initializer. This is discussed later in this chapter.

Default Constructors

A default constructor is a constructor that requires no arguments. It is also called a 0-argument constructor. With a default constructor, you can give initial values to data members even though the client did not specify them.

When You Need a Default Constructor

Consider arrays of objects. The act of creating an array of objects accomplishes two tasks: it allocates contiguous memory space for all the objects and it calls the default constructor on each object. C++ fails to provide any syntax to tell the array creation code directly to call a different constructor. For example, if you do not define a default constructor for the SpreadsheetCell class, the following code does not compile:

SpreadsheetCell cells[3]; // FAILS compilation without default constructor
SpreadsheetCell* myCellp = new SpreadsheetCell[10]; // Also FAILS

You can circumvent this restriction for stack-based arrays by using initializers like these:

SpreadsheetCell cells[3] = {SpreadsheetCell(0), SpreadsheetCell(23),
    SpreadsheetCell(41)};

However, it is usually easier to ensure that your class has a default constructor if you intend to create arrays of objects of that class. If you haven’t defined your own constructors, the compiler automatically creates a default constructor for you. This compiler-generated constructor is discussed in a later section.

A default constructor is also required for classes that you want to store in Standard Library containers, such as std::vector.

Default constructors are also useful when you want to create objects of that class inside other classes, which is shown later in this chapter in the section, “Constructor Initializers.”

How to Write a Default Constructor

Here is part of the SpreadsheetCell class definition with a default constructor:

class SpreadsheetCell
{
    public:
        SpreadsheetCell();
        // Remainder of the class definition omitted for brevity
};

Here is a first crack at an implementation of the default constructor:

SpreadsheetCell::SpreadsheetCell()
{
    mValue = 0;
}

If you use an in-class member initializer for mValue, then the single statement in this default constructor can be left out:

SpreadsheetCell::SpreadsheetCell()
{
}

You use the default constructor on the stack like this:

SpreadsheetCell myCell;
myCell.setValue(6);
cout << "cell 1: " << myCell.getValue() << endl;

The preceding code creates a new SpreadsheetCell called myCell, sets its value, and prints out its value. Unlike other constructors for stack-based objects, you do not call the default constructor with function-call syntax. Based on the syntax for other constructors, you might be tempted to call the default constructor like this:

SpreadsheetCell myCell(); // WRONG, but will compile.
myCell.setValue(6);       // However, this line will not compile.
cout << "cell 1: " << myCell.getValue() << endl;

Unfortunately, the line attempting to call the default constructor compiles. The line following it does not compile. This problem is commonly known as the most vexing parse, and it means that your compiler thinks the first line is actually a function declaration for a function with the name myCell that takes zero arguments and returns a SpreadsheetCell object. When it gets to the second line, it thinks that you’re trying to use a function name as an object!

For heap-based object allocation, the default constructor can be used as follows:

auto smartCellp = make_unique<SpreadsheetCell>();
// Or with a raw pointer (not recommended)
SpreadsheetCell* myCellp = new SpreadsheetCell();
// Or
// SpreadsheetCell* myCellp = new SpreadsheetCell;
// … use myCellp
delete myCellp;    myCellp = nullptr;

Compiler-Generated Default Constructor

The first SpreadsheetCell class definition in this chapter looked like this:

class SpreadsheetCell
{
    public:
        void setValue(double inValue);
        double getValue() const;
    private:
        double mValue;
};

This definition does not declare a default constructor, but still, the code that follows works fine:

SpreadsheetCell myCell;
myCell.setValue(6);

The following definition is the same as the preceding definition except that it adds an explicit constructor, accepting a double. It still does not explicitly declare a default constructor.

class SpreadsheetCell
{
    public:
        SpreadsheetCell(double initialValue); // No default constructor
        // Remainder of the class definition omitted for brevity
};

With this definition, the following code does not compile anymore:

SpreadsheetCell myCell;
myCell.setValue(6);

What’s going on here? The reason it is not compiling is that if you don’t specify any constructors, the compiler writes one for you that doesn’t take any arguments. This compiler-generated default constructor calls the default constructor on all object members of the class, but does not initialize the language primitives such as int and double. Nonetheless, it allows you to create objects of that class. However, if you declare a default constructor, or any other constructor, the compiler no longer generates a default constructor for you.

Explicitly Defaulted Constructors

In C++03 or older, if your class required a number of explicit constructors accepting arguments but also a default constructor that did nothing, you still had to explicitly write your empty default constructor as shown earlier.

To avoid having to write empty default constructors manually, C++ supports the concept of explicitly defaulted constructors. This allows you to write the class definition as follows without the need to implement the default constructor in the implementation file:

class SpreadsheetCell
{
    public:
        SpreadsheetCell() = default;
        SpreadsheetCell(double initialValue);
        SpreadsheetCell(std::string_view initialValue);
        // Remainder of the class definition omitted for brevity
};

SpreadsheetCell defines two custom constructors. However, the compiler still generates a standard compiler-generated default constructor due to the use of the default keyword.

Explicitly Deleted Constructors

C++ also supports the concept of explicitly deleted constructors. For example, you can define a class with only static methods (see Chapter 9) for which you do not want to write any constructors and you also do not want the compiler to generate the default constructor. In that case, you need to explicitly delete the default constructor:

class MyClass
{
    public:
        MyClass() = delete;
};

Constructor Initializers

Up to now, this chapter initialized data members in the body of a constructor, as in this example:

SpreadsheetCell::SpreadsheetCell(double initialValue)
{
    setValue(initialValue);
}

C++ provides an alternative method for initializing data members in the constructor, called the constructor initializer, also known as the ctor-initializer or member initializer list. Here is the same SpreadsheetCell constructor, rewritten to use the ctor-initializer syntax:

SpreadsheetCell::SpreadsheetCell(double initialValue)
    : mValue(initialValue)
{
}

As you can see, the ctor-initializer appears syntactically between the constructor argument list and the opening brace for the body of the constructor. The list starts with a colon and is separated by commas. Each element in the list is an initialization of a data member using function notation or the uniform initialization syntax, a call to a base class constructor (see Chapter 10), or a call to a delegated constructor, which is discussed later.

Initializing data members with a ctor-initializer provides different behavior than does initializing data members inside the constructor body itself. When C++ creates an object, it must create all the data members of the object before calling the constructor. As part of creating these data members, it must call a constructor on any of them that are themselves objects. By the time you assign a value to an object inside your constructor body, you are not actually constructing that object. You are only modifying its value. A ctor-initializer allows you to provide initial values for data members as they are created, which is more efficient than assigning values to them later.

If your class has as data member an object of a class that has a default constructor, then you do not have to explicitly initialize the object in the ctor-initializer. For example, if you have an std::string as data member, its default constructor initializes the string to the empty string, so initializing it to "" in the ctor-initializer is superfluous.

On the other hand, if your class has as data member an object of a class without a default constructor, you have to use the ctor-initializer to properly construct that object. For example, take the following SpreadsheetCell class:

class SpreadsheetCell 
{
    public:
        SpreadsheetCell(double d);
};

This class only has one explicit constructor accepting a double and does not include a default constructor. You can use this class as a data member of another class as follows:

class SomeClass 
{
    public:
        SomeClass();
    private:
        SpreadsheetCell mCell;
};

And you can implement the SomeClass constructor as follows:

SomeClass::SomeClass() { }

However, with this implementation, the code does not compile. The compiler does not know how to initialize the mCell data member of SomeClass because it does not have a default constructor.

You have to initialize the mCell data member in the ctor-initializer as follows:

SomeClass::SomeClass() : mCell(1.0) { }

Some programmers prefer to assign initial values in the body of the constructor, even though this might be less efficient. However, several data types must be initialized in a ctor-initializer or with an in-class initializer. The following table summarizes them:

DATA TYPE EXPLANATION
const data members You cannot legally assign a value to a const variable after it is created. Any value must be supplied at the time of creation.
Reference data members References cannot exist without referring to something.
Object data members for which there is no default constructor C++ attempts to initialize member objects using a default constructor. If no default constructor exists, it cannot initialize the object.
Base classes without default constructors These are covered in Chapter 10.

There is one important caveat with ctor-initializers: they initialize data members in the order that they appear in the class definition, not their order in the ctor-initializer. Take the following definition for a class called Foo. Its constructor simply stores a double value and prints out the value to the console.

class Foo
{
    public:
        Foo(double value);
    private:
        double mValue;
};

Foo::Foo(double value) : mValue(value)
{
    cout << "Foo::mValue = " << mValue << endl;
}

Suppose you have another class, MyClass, that contains a Foo object as one of its data members:

class MyClass
{
    public:
        MyClass(double value);
    private:
        double mValue;
        Foo mFoo;
};

Its constructor could be implemented as follows:

MyClass::MyClass(double value) : mValue(value), mFoo(mValue)
{
    cout << "MyClass::mValue = " << mValue << endl;
}

The ctor-initializer first stores the given value in mValue, and then calls the Foo constructor with mValue as argument. You can create an instance of MyClass as follows:

MyClass instance(1.2);

Here is the output of the program:

Foo::mValue = 1.2
MyClass::mValue = 1.2

So, everything looks fine. Now make one tiny change to the MyClass definition. You just reverse the order of the mValue and mFoo data members. Nothing else is changed.

class MyClass
{
    public:
        MyClass(double value);
    private:
        Foo mFoo;
        double mValue;
};

The output of the program now depends on your system. It could, for example, be as follows:

Foo::mValue = -9.25596e+61
MyClass::mValue = 1.2

This is far from what you would expect. You might assume, based on your ctor-initializer, that mValue is initialized before using mValue in the call to the Foo constructor. But C++ doesn’t work that way. The data members are initialized in the order they appear in the definition of the class, not the order in the ctor-initializer! So, in this case, the Foo constructor is called first with an uninitialized mValue.

Note that some compilers issue a warning when the order in the class definition does not match the order in the ctor-initializer.

Copy Constructors

There is a special constructor in C++ called a copy constructor that allows you to create an object that is an exact copy of another object. If you don’t write a copy constructor, C++ generates one for you that initializes each data member in the new object from its equivalent data member in the source object. For object data members, this initialization means that their copy constructors are called.

Here is the declaration for a copy constructor in the SpreadsheetCell class:

class SpreadsheetCell
{
    public:
        SpreadsheetCell(const SpreadsheetCell& src);
        // Remainder of the class definition omitted for brevity
};

The copy constructor takes a const reference to the source object. Like other constructors, it does not return a value. Inside the constructor, you should copy all the data members from the source object. Technically, of course, you can do whatever you want in the copy constructor, but it’s generally a good idea to follow expected behavior and initialize the new object to be a copy of the old one. Here is a sample implementation of the SpreadsheetCell copy constructor. Note the use of the ctor-initializer.

SpreadsheetCell::SpreadsheetCell(const SpreadsheetCell& src)
    : mValue(src.mValue)
{
}

Given a set of data members, called m1, m2, … mn, the compiler-generated copy constructor can be expressed as follows:

classname::classname(const classname& src)
    : m1(src.m1), m2(src.m2), … mn(src.mn) { }

Therefore, in most circumstances, there is no need for you to specify a copy constructor!

When the Copy Constructor Is Called

The default semantics for passing arguments to functions in C++ is pass-by-value. That means that the function or method receives a copy of the value or object. Thus, whenever you pass an object to a function or method, the compiler calls the copy constructor of the new object to initialize it. For example, suppose you have the following printString() function accepting a string parameter by value:

void printString(string inString)
{
    cout << inString << endl;
}

Recall that the C++ string is actually a class, not a built-in type. When your code makes a call to printString() passing a string argument, the string parameter inString is initialized with a call to its copy constructor. The argument to the copy constructor is the string you passed to printString(). In the following example, the string copy constructor is executed for the inString object in printString() with name as its parameter.

string name = "heading one";
printString(name); // Copies name

When the printString() method finishes, inString is destroyed. Because it was only a copy of name, name remains intact. Of course, you can avoid the overhead of copy constructors by passing parameters as const references.

When returning objects by value from a function, the copy constructor might also get called. This is discussed in the section “Objects as Return Values” later in this chapter.

Calling the Copy Constructor Explicitly

You can use the copy constructor explicitly as well. It is often useful to be able to construct one object as an exact copy of another. For example, you might want to create a copy of a SpreadsheetCell object like this:

SpreadsheetCell myCell1(4);
SpreadsheetCell myCell2(myCell1); // myCell2 has the same values as myCell1

Passing Objects by Reference

In order to avoid copying objects when you pass them to functions and methods, you should declare that the function or method takes a reference to the object. Passing objects by reference is usually more efficient than passing them by value, because only the address of the object is copied, not the entire contents of the object. Additionally, pass-by-reference avoids problems with dynamic memory allocation in objects, which is discussed in Chapter 9.

When you pass an object by reference, the function or method using the object reference could change the original object. When you are only using pass-by-reference for efficiency, you should preclude this possibility by declaring the object const as well. This is known as passing objects by const reference and has been done in examples throughout this book.

Note that the SpreadsheetCell class has a number of methods accepting an std::string_view as parameter. As discussed in Chapter 2, a string_view is basically just a pointer and a length. So, it is very cheap to copy, and is usually passed by value.

Also primitive types, such as int, double, and so on, should just be passed by value. You don’t gain anything by passing such types by const reference.

The doubleToString() method of the SpreadsheetCell class always returns a string by value because the implementation of the method creates a local string object that at the end of the method is returned to the caller. Returning a reference to this string wouldn’t work because the string to which it references will be destroyed when the function exits.

Explicitly Defaulted and Deleted Copy Constructor

You can explicitly default or delete a compiler-generated copy constructor as follows:

SpreadsheetCell(const SpreadsheetCell& src) = default;

or

SpreadsheetCell(const SpreadsheetCell& src) = delete;

By deleting the copy constructor, the object cannot be copied anymore. This can be used to disallow passing the object by value, as discussed in Chapter 9.

Initializer-List Constructors

An initializer-list constructor is a constructor with an std::initializer_list<T> as first parameter, without any additional parameters or with additional parameters having default values. Before you can use the std::initializer_list<T> template, you need to include the <initializer_list> header. The following class demonstrates its use. The class accepts only an initializer_list<T> with an even number of elements; otherwise, it throws an exception.

class EvenSequence
{
    public:
        EvenSequence(initializer_list<double> args)
        {
            if (args.size() % 2 != 0) {
                throw invalid_argument("initializer_list should "
                    "contain even number of elements.");
            }
            mSequence.reserve(args.size());
            for (const auto& value : args) {
                mSequence.push_back(value);
            }
        }

        void dump() const
        {
            for (const auto& value : mSequence) {
                cout << value << ", ";
            }
            cout << endl;
        }
    private:
        vector<double> mSequence;
};

Inside the initializer-list constructor you can access the elements of the initializer-list with a range-based for loop. You can get the number of elements in the initializer-list with the size() method.

The EvenSequence initializer-list constructor uses a range-based for loop to copy elements from the given initializer_list<T>. You can also use the assign() method of vector. The different methods of vector, including assign(), are discussed in detail in Chapter 17. To give you an idea of the power of a vector, here is the initializer-list constructor using assign():

EvenSequence(initializer_list<double> args)
{
    if (args.size() % 2 != 0) {
        throw invalid_argument("initializer_list should "
            "contain even number of elements.");
    }
    mSequence.assign(args);
}
EvenSequence objects can be constructed as follows:

EvenSequence p1 = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0};
p1.dump();

try {
    EvenSequence p2 = {1.0, 2.0, 3.0};
} catch (const invalid_argument& e) {
    cout << e.what() << endl;
}

The construction of p2 throws an exception because it has an odd number of elements in the initializer-list. The preceding equal signs are optional and can be left out, as in this example:

EvenSequence p1{1.0, 2.0, 3.0, 4.0, 5.0, 6.0};

The Standard Library has full support for initializer-list constructors. For example, the std::vector container can be initialized with an initializer-list:

std::vector<std::string> myVec = {"String 1", "String 2", "String 3"};

Without initializer-list constructors, one way to initialize this vector is by using several push_back() calls:

std::vector<std::string> myVec;
myVec.push_back("String 1");
myVec.push_back("String 2");
myVec.push_back("String 3");

Initializer lists are not limited to constructors and can also be used with normal functions as explained in Chapter 1.

Delegating Constructors

Delegating constructors allow constructors to call another constructor from the same class. However, this call cannot be placed in the constructor body; it must be in the ctor-initializer and it must be the only member-initializer in the list. Following is an example:

SpreadsheetCell::SpreadsheetCell(string_view initialValue)
    : SpreadsheetCell(stringToDouble(initialValue))
{
}

When this string_view constructor (the delegating constructor) is called, it first delegates the call to the target constructor, which is the double constructor in this example. When the target constructor returns, the body of the delegating constructor is executed.

Make sure you avoid constructor recursion while using delegate constructors. Here is an example:

class MyClass
{
    MyClass(char c) : MyClass(1.2) { }
    MyClass(double d) : MyClass('m') { }
};

The first constructor delegates to the second constructor, which delegates back to the first one. The behavior of such code is undefined by the standard and depends on the compiler.

Summary of Compiler-Generated Constructors

The compiler can automatically generate a default constructor and a copy constructor for every class. However, the constructors that the compiler automatically generates depend on the constructors that you define yourself according to the rules in the following table.

IF YOU DEFINE . . . . . . THEN THE COMPILER GENERATES . . . . . . AND YOU CAN CREATE AN OBJECT . . .
[no constructors] A default constructor
A copy constructor
With no arguments:
SpreadsheetCell cell;
As a copy of another object:
SpreadsheetCell myCell(cell);
A default constructor only A copy constructor With no arguments:
SpreadsheetCell cell;
As a copy of another object:
SpreadsheetCell myCell(cell);
A copy constructor only No constructors Theoretically, as a copy of another object. Practically, you can’t create any objects, because there are no non-copy constructors.
A single-argument or multi-argument non-copy constructor only A copy constructor With arguments:
SpreadsheetCell cell(6);
As a copy of another object:
SpreadsheetCell myCell(cell);
A default constructor as well as a single-argument or multi-argument non-copy constructor A copy constructor With no arguments:
SpreadsheetCell cell;
With arguments:
SpreadsheetCell myCell(5);
As a copy of another object:
SpreadsheetCell anotherCell(cell);

Note the lack of symmetry between the default constructor and the copy constructor. As long as you don’t define a copy constructor explicitly, the compiler creates one for you. On the other hand, as soon as you define any constructor, the compiler stops generating a default constructor.

As mentioned before in this chapter, the automatic generation of a default constructor and a default copy constructor can be influenced by defining them as explicitly defaulted or explicitly deleted.

Object Destruction

When an object is destroyed, two events occur: the object’s destructor method is called, and the memory it was taking up is freed. The destructor is your chance to perform any cleanup work for the object, such as freeing dynamically allocated memory or closing file handles. If you don’t declare a destructor, the compiler writes one for you that does recursive member-wise destruction and allows the object to be deleted. The section on dynamic memory allocation in Chapter 9 shows you how to write a destructor.

Objects on the stack are destroyed when they go out of scope, which means whenever the current function, method, or other execution block ends. In other words, whenever the code encounters an ending curly brace, any objects created on the stack within those curly braces are destroyed. The following program shows this behavior:

int main()
{
    SpreadsheetCell myCell(5);
    if (myCell.getValue() == 5) {
        SpreadsheetCell anotherCell(6);
    } // anotherCell is destroyed as this block ends.

    cout << "myCell: " << myCell.getValue() << endl;
    return 0;
} // myCell is destroyed as this block ends.

Objects on the stack are destroyed in the reverse order of their declaration (and construction). For example, in the following code fragment, myCell2 is created before anotherCell2, so anotherCell2 is destroyed before myCell2 (note that you can start a new code block at any point in your program with an opening curly brace):

{
    SpreadsheetCell myCell2(4);
    SpreadsheetCell anotherCell2(5); // myCell2 constructed before anotherCell2
} // anotherCell2 destroyed before myCell2

This ordering also applies to objects that are data members of other objects. Recall that data members are initialized in the order of their declaration in the class. Thus, following the rule that objects are destroyed in the reverse order of their construction, data member objects are destroyed in the reverse order of their declaration in the class.

Objects allocated on the heap without the help of smart pointers are not destroyed automatically. You must call delete on the object pointer to call its destructor and free the memory. The following program shows this behavior:

int main()
{
    SpreadsheetCell* cellPtr1 = new SpreadsheetCell(5);
    SpreadsheetCell* cellPtr2 = new SpreadsheetCell(6);
    cout << "cellPtr1: " << cellPtr1->getValue() << endl;
    delete cellPtr1; // Destroys cellPtr1
    cellPtr1 = nullptr;
    return 0;
} // cellPtr2 is NOT destroyed because delete was not called on it.

Assigning to Objects

Just as you can assign the value of one int to another in C++, you can assign the value of one object to another. For example, the following code assigns the value of myCell to anotherCell:

SpreadsheetCell myCell(5), anotherCell;
anotherCell = myCell;

You might be tempted to say that myCell is “copied” to anotherCell. However, in the world of C++, “copying” only occurs when an object is being initialized. If an object already has a value that is being overwritten, the more accurate term is “assigned to.” Note that the facility that C++ provides for copying is the copy constructor. Because it is a constructor, it can only be used for object creation, not for later assignments to the object.

Therefore, C++ provides another method in every class to perform assignment. This method is called the assignment operator. Its name is operator= because it is actually an overloading of the = operator for that class. In the preceding example, the assignment operator for anotherCell is called, with myCell as the argument.

As usual, if you don’t write your own assignment operator, C++ writes one for you to allow objects to be assigned to one another. The default C++ assignment behavior is almost identical to its default copying behavior: it recursively assigns each data member from the source to the destination object.

Declaring an Assignment Operator

Here is the assignment operator for the SpreadsheetCell class:

class SpreadsheetCell
{
    public:
        SpreadsheetCell& operator=(const SpreadsheetCell& rhs);
        // Remainder of the class definition omitted for brevity
};

The assignment operator often takes a const reference to the source object, like the copy constructor. In this case, the source object is called rhs, which stands for right-hand side of the equals sign, but you are free to call it whatever you want. The object on which the assignment operator is called is the left-hand side of the equals sign.

Unlike a copy constructor, the assignment operator returns a reference to a SpreadsheetCell object. The reason is that assignments can be chained, as in the following example:

myCell = anotherCell = aThirdCell;

When that line is executed, the first thing that happens is the assignment operator for anotherCell is called with aThirdCell as its “right-hand side” parameter. Next, the assignment operator for myCell is called. However, its parameter is not anotherCell. Its right-hand side is the result of the assignment of aThirdCell to anotherCell. If that assignment fails to return a result, there is nothing to pass to myCell.

You might be wondering why the assignment operator for myCell can’t just take anotherCell. The reason is that using the equals sign is actually just shorthand for what is really a method call. When you look at the line in its full functional syntax, you can see the problem:

myCell.operator=(anotherCell.operator=(aThirdCell));

Now, you can see that the operator= call from anotherCell must return a value, which is passed to the operator= call for myCell. The correct value to return is anotherCell itself, so it can serve as the source for the assignment to myCell. However, returning anotherCell directly would be inefficient, so you can return a reference to anotherCell.

Defining an Assignment Operator

The implementation of the assignment operator is similar to that of a copy constructor, with several important differences. First, a copy constructor is called only for initialization, so the destination object does not yet have valid values. An assignment operator can overwrite the current values in an object. This consideration doesn’t really come into play until you have dynamically allocated memory in your objects. See Chapter 9 for details.

Second, it’s legal in C++ to assign an object to itself. For example, the following code compiles and runs:

SpreadsheetCell cell(4);
cell = cell; // Self-assignment

Your assignment operator needs to take the possibility of self-assignment into account. In the SpreadsheetCell class, this is not important, as its only data member is a primitive type, double. However, when your class has dynamically allocated memory or other resources, it’s paramount to take self-assignment into account, as is discussed in detail in Chapter 9. To prevent problems in such cases, assignment operators usually check for self-assignment at the beginning of the method and return immediately.

Here is the start of the definition of the assignment operator for the SpreadsheetCell class:

SpreadsheetCell& SpreadsheetCell::operator=(const SpreadsheetCell& rhs)
{
    if (this == &rhs) {

This first line checks for self-assignment, but it might be a bit cryptic. Self-assignment occurs when the left-hand side and the right-hand side of the equals sign are the same. One way to tell if two objects are the same is if they occupy the same memory location—more explicitly, if pointers to them are equal. Recall that this is a pointer to an object accessible from any method called on the object. Thus, this is a pointer to the left-hand side object. Similarly, &rhs is a pointer to the right-hand side object. If these pointers are equal, the assignment must be self-assignment, but because the return type is SpreadsheetCell&, a correct value must still be returned. All assignment operators return *this, and the self-assignment case is no exception:

        return *this;
    }

this is a pointer to the object on which the method executes, so *this is the object itself. The compiler returns a reference to the object to match the declared return value. Now, if it is not self-assignment, you have to do an assignment to every member:

    mValue = rhs.mValue;
    return *this;
}

Here the method copies the values, and finally, it returns *this, as explained previously.

Explicitly Defaulted and Deleted Assignment Operator

You can explicitly default or delete a compiler-generated assignment operator as follows:

SpreadsheetCell& operator=(const SpreadsheetCell& rhs) = default;

or

SpreadsheetCell& operator=(const SpreadsheetCell& rhs) = delete;

Compiler-Generated Copy Constructor and Copy Assignment Operator

C++11 has deprecated the generation of a copy constructor if the class has a user-declared copy assignment operator or destructor. If you still need a compiler-generated copy constructor in such a case, you can explicitly default one:

MyClass(const MyClass& src) = default;

C++11 has also deprecated the generation of a copy assignment operator if the class has a user-declared copy constructor or destructor. If you still need a compiler-generated copy assignment operator in such a case, you can explicitly default one:

MyClass& operator=(const MyClass& rhs) = default;

Distinguishing Copying from Assignment

It is sometimes difficult to tell when objects are initialized with a copy constructor rather than assigned to with the assignment operator. Essentially, things that look like a declaration are going to be using copy constructors, and things that look like assignment statements are handled by the assignment operator. Consider the following code:

SpreadsheetCell myCell(5);
SpreadsheetCell anotherCell(myCell);

AnotherCell is constructed with the copy constructor.

SpreadsheetCell aThirdCell = myCell;

aThirdCell is also constructed with the copy constructor, because this is a declaration. The operator= is not called for this line! This syntax is just another way to write SpreadsheetCell aThirdCell(myCell);. However, consider the following code:

anotherCell = myCell; // Calls operator= for anotherCell

Here, anotherCell has already been constructed, so the compiler calls operator=.

Objects as Return Values

When you return objects from functions or methods, it is sometimes difficult to see exactly what copying and assignment is happening. For example, the implementation of SpreadsheetCell:: getString() looks like this:

string SpreadsheetCell::getString() const
{
    return doubleToString(mValue);
}

Now consider the following code:

SpreadsheetCell myCell2(5);
string s1;
s1 = myCell2.getString();

When getString() returns the string, the compiler actually creates an unnamed temporary string object by calling a string copy constructor. When you assign this result to s1, the assignment operator is called for s1 with the temporary string as a parameter. Then, the temporary string object is destroyed. Thus, the single line of code invokes the copy constructor and the assignment operator (for two different objects). However, compilers are free and sometimes required to implement Return Value Optimization (RVO), also known as copy elision, to optimize away costly copy constructions when returning values.

In case you’re not confused enough, consider this code:

SpreadsheetCell myCell3(5);
string s2 = myCell3.getString();

In this case, getString() still creates a temporary unnamed string object when it returns. But now, s2 gets its copy constructor called, not its assignment operator.

With move semantics, the compiler can use a move constructor instead of a copy constructor to return the string from getString(). This is more efficient and is discussed in Chapter 9.

If you ever forget the order in which these things happen or which constructor or operator is called, you can easily figure it out by temporarily including helpful output in your code or by stepping through it with a debugger.

Copy Constructors and Object Members

You should also note the difference between assignment and copy constructor calls in constructors. If an object contains other objects, the compiler-generated copy constructor calls the copy constructors of each of the contained objects recursively. When you write your own copy constructor, you can provide the same semantics by using a ctor-initializer, as shown previously. If you omit a data member from the ctor-initializer, the compiler performs default initialization on it (a call to the default constructor for objects) before executing your code in the body of the constructor. Thus, by the time the body of the constructor executes, all object data members have already been initialized.

For example, you could write your copy constructor like this:

SpreadsheetCell::SpreadsheetCell(const SpreadsheetCell& src)
{
    mValue = src.mValue;
}

However, when you assign values to data members in the body of the copy constructor, you are using the assignment operator on them, not the copy constructor, because they have already been initialized, as described previously.

If you write the copy constructor as follows, then mValue is initialized using the copy constructor:

SpreadsheetCell::SpreadsheetCell(const SpreadsheetCell& src)
    : mValue(src.mValue)
{
}

SUMMARY

This chapter covered the fundamental aspects of C++’s facilities for object-oriented programming: classes and objects. It first reviewed the basic syntax for writing classes and using objects, including access control. Then, it covered object life cycles: when objects are constructed, destructed, and assigned to, and what methods those actions invoke. The chapter included details of the constructor syntax, including ctor-initializers and initializer-list constructors, and introduced the notion of copy assignment operators. It also specified exactly which constructors the compiler writes for you, and under what circumstances, and explained that default constructors require no arguments.

You may have found this chapter to be mostly review. Or, it may have opened your eyes to the world of object-oriented programming in C++. In any case, now that you are proficient with objects and classes, read Chapter 9 to learn more about their tricks and subtleties.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset