Gaining Proficiency with Classes and Objects
As an object-oriented language, C++ provides facilities for using objects and for writing object blueprints, called classes. You can certainly write programs in C++ without classes and objects, but by doing so, you do not take advantage of the most fundamental and useful aspect of the language; writing a C++ program without classes is like traveling to Paris and eating at McDonald’s. To use classes and objects effectively, you must understand their syntax and capabilities.
Chapter 1, “A Crash Course in C++ and the Standard Library,” reviewed the basic syntax of class definitions. Chapter 5, “Designing with Classes,” introduced the object-oriented approach to programming in C++ and presented specific design strategies for classes and objects. This chapter describes the fundamental concepts involved in using classes and objects, including writing class definitions, defining member functions, using objects on the stack and the free store, writing constructors, default constructors, compiler-generated constructors, constructor initializers (also known as ctor-initializers), copy constructors, initializer-list constructors, destructors, and assignment operators. Even if you are already comfortable with classes and objects, you should skim this chapter because it contains various tidbits of information with which you might not yet be familiar.
INTRODUCING THE SPREADSHEET EXAMPLE
Section titled “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 it 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 class. Chapter 9, “Mastering Classes and Objects,” develops the Spreadsheet and SpreadsheetApplication classes.
WRITING CLASSES
Section titled “WRITING CLASSES”When you write a class, you specify the behaviors, or member functions, that will apply to objects of that class, and you specify 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 member functions.
Class Definitions
Section titled “Class Definitions”Here is a first attempt at a simple SpreadsheetCell class in a spreadsheet_cell module, in which each cell can store only a single number:
export module spreadsheet_cell;
export class SpreadsheetCell{ public: void setValue(double value); double getValue() const; private: double m_value;};As described in Chapter 1, the first line specifies that this is the definition of a module named spreadsheet_cell. Every class definition begins with the keyword class followed by the name of the class. If the class is defined in a module and the class must become visible to clients importing the module, then the class keyword is prefixed with export. A class definition is a declaration and ends with a semicolon.
Class definitions usually go in a file named after the class. For example, the SpreadsheetCell class definition is put in a file called SpreadsheetCell.cppm. Some compilers require the use of a specific extension; others allow you to choose any extension.
Class Members
Section titled “Class Members”A class can have several members. A member can be a member function (which in turn is a function, 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 member functions that this class supports:
void setValue(double value);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, like the getValue() member function.
The line that looks like a variable declaration declares the data member for this class:
double m_value;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 m_value data member. 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
Section titled “Access Control”Every member in a class is subject to one of three access specifiers: public, private, or protected. The protected access specifier is explained in the context of inheritance in Chapter 10, “Discovering Inheritance Techniques.” An access specifier applies to all member declarations that follow it, until the next access specifier. In the SpreadsheetCell class, the setValue() and getValue() member functions have public access, while the m_value 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 to after the setValue() member function declaration gives the setValue() member function private access instead of public:
export class SpreadsheetCell{ void setValue(double value); // now has private access public: double getValue() const; private: double m_value;};In C++, a struct can have member functions just like a class. In fact, there is only one difference: for a struct, the default access specifier is public, while it’s private for a class.
For example, the SpreadsheetCell class could be rewritten using a struct as follows:
export struct SpreadsheetCell{ void setValue(double value); double getValue() const; private: double m_value;};However, it’s unconventional to do so. A struct is usually used only if you just need a collection of publicly accessible data members and no member functions. The following is an example of such a simple struct to store 2-D point coordinates:
export struct Point{ double x; double y;};Order of Declarations
Section titled “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:
export class SpreadsheetCell{ public: void setValue(double value); private: double m_value; public: double getValue() const;};However, for clarity it is a good idea to group declarations based on their access specifier and to group member functions and data members within those declarations.
In-Class Member Initializers
Section titled “In-Class Member Initializers”Data members can be initialized directly in the class definition. For example, the SpreadsheetCell class can, by default, initialize m_value to 0 directly in the class definition as follows:
export class SpreadsheetCell{ // Remainder of the class definition omitted for brevity private: double m_value { 0 };};Defining Member Functions
Section titled “Defining Member Functions”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() member function, your linker will complain that those member functions are not defined. That’s because these member functions only have prototypes so far, but no implementations yet. Usually, a class definition goes in a module interface file. For the member function definitions, you have a choice: they can go in the module interface file or in a module implementation file.
Here is the SpreadsheetCell class with in-class member function implementations:
export module spreadsheet_cell;
export class SpreadsheetCell{ public: void setValue(double value) { m_value = value; } double getValue() const { return m_value; } private: double m_value { 0 };};Unlike with header files, with C++ modules there is no harm in putting member function definitions in module interface files. This is discussed in more detail in Chapter 11, “Modules, Header Files, and Miscellaneous Topics.” However, this book often puts member function definitions in module implementation files, in the interest of keeping module interface files clean and without any implementation details.
The first line of a module implementation file specifies which module the implementations are for. Here are the definitions for the two member functions of the SpreadsheetCell class in the spreadsheet_cell module:
module spreadsheet_cell;
void SpreadsheetCell::setValue(double value){ m_value = value;}
double SpreadsheetCell::getValue() const{ return m_value;}Note that the name of the class followed by two colons precedes each member function name:
void SpreadsheetCell::setValue(double value)The :: is called the scope resolution operator. In this context, the syntax tells the compiler that the coming definition of the setValue() member function is part of the SpreadsheetCell class. Note also that you do not repeat the access specification when you define the member function.
Accessing Data Members
Section titled “Accessing Data Members”Non-static member functions of a class, such as setValue() and getValue(), are always executed on behalf of a specific object of that class. Inside a member function’s 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 m_value variable inside whatever object calls the member function:
m_value = value;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 Member Functions
Section titled “Calling Other Member Functions”You can call member functions of a class from inside another member function. For example, consider an extension to the SpreadsheetCell class to allow setting and retrieving the value of a cell as a string or as a number. When you try to set the value of a cell with a string, the cell tries to convert the string to a number. If the string 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 such a class definition for a SpreadsheetCell:
export module spreadsheet_cell;import std;export class SpreadsheetCell{ public: void setValue(double value); double getValue() const;
void setString(std::string_view value); std::string getString() const; private: std::string doubleToString(double value) const; double stringToDouble(std::string_view value) const; double m_value { 0 };};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. The class definition shows two new member functions to set and retrieve the text representation of the cell, and two new private helper member functions to convert a double to a string and vice versa. Here are the implementations of all the member functions:
module spreadsheet_cell;import std;using namespace std;
void SpreadsheetCell::setValue(double value){ m_value = value;}
double SpreadsheetCell::getValue() const{ return m_value;}
void SpreadsheetCell::setString(string_view value){ m_value = stringToDouble(value);}
string SpreadsheetCell::getString() const{ return doubleToString(m_value);}
string SpreadsheetCell::doubleToString(double value) const{ return to_string(value);}
double SpreadsheetCell::stringToDouble(string_view value) const{ double number { 0 }; from_chars(value.data(), value.data() + value.size(), number); return number;}The std::to_string() and from_chars() functions are explained in Chapter 2, “Working with Strings and String Views.”
Note that with this implementation of the doubleToString() member function, a value of, for example, 6.1 is converted to 6.100000. However, because it is a private helper member function, you are free to modify the implementation without having to modify any client code.
Using Objects
Section titled “Using Objects”The previous class definition says that a SpreadsheetCell consists of one data member, four public member functions, and two private member functions. However, the class definition does not actually create any SpreadsheetCells; it just specifies their shape and behavior. In that sense, a class is like 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 free store.
Objects on the Stack
Section titled “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");println("cell 1: {}", myCell.getValue());println("cell 2: {}", anotherCell.getValue());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, also called the member access operator; it allows you to call public member functions 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: 6cell 2: 3.2Objects on the Free Store
Section titled “Objects on the Free Store”You can also dynamically allocate objects by using new:
SpreadsheetCell* myCellp { new SpreadsheetCell { } };myCellp->setValue(3.7);println("cell 1: {} {}", myCellp->getValue(), myCellp->getString());delete myCellp;myCellp = nullptr;When you create an object on the free store, 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);println("cell 1: {} {}", (*myCellp).getValue(), (*myCellp).getString());delete myCellp;myCellp = nullptr;Just as you must free other memory that you allocate on the free store, you must free the memory for objects that you allocate on the free store by calling delete on them, as is done in the previous code snippets! To guarantee safety and to avoid memory problems, you really 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);println("cell 1: {} {}", myCellp->getValue(), myCellp->getString());With smart pointers you don’t need to manually free the memory; it happens automatically.
When you allocate an object with new, free it with delete after you are finished with it, or, better yet, use smart pointers to manage the memory automatically!
The this Pointer
Section titled “The this Pointer”Every normal member function call implicitly 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 member functions, and you can pass it to other member functions 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 m_value. In that case, setValue() would look like the following:
void SpreadsheetCell::setValue(double value){ value = value; // Confusing!}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?
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, “Coding with Style,” you will never encounter this type of name collision.
You can also use the this pointer to call a function that takes, as a parameter, a pointer to an object from within a member function of that object. For example, suppose you write a printCell() stand-alone function (not a member function) like this:
void printCell(const SpreadsheetCell& cell){ println("{}", cell.getString());}If you want to call printCell() from the setValue() member function, 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);} Explicit Object Parameter
Section titled “ Explicit Object Parameter”Starting with C++23, instead of relying on the compiler to provide an implicit this parameter, you can use an explicit object parameter, usually of a reference type. The following code snippet implements the setValue() member function of SpreadsheetCell from the previous section using an explicit object parameter:
void SpreadsheetCell::setValue(this SpreadsheetCell& self, double value){ self.m_value = value; printCell(self);}The first parameter of setValue() is now the explicit object parameter, usually called self, but you can use any name you want. The type of self is prefixed with the this keyword. This explicit object parameter must be the first parameter of the member function. Once you use an explicit object parameter, the function no longer has an implicitly defined this; hence, in the body of setValue(), you now must explicitly use self to access anything from the SpreadsheetCell.
Calling a member function that uses an explicit object parameter is no different than calling one with an implicit this parameter. Even though setValue() now specifies two parameters, self and value, you still call it by passing just a single argument, the value that you want to set:
SpreadsheetCell myCell;myCell.setValue(6);Using explicit object parameters as demonstrated in this section has no benefits at all, it even makes the code more verbose. However, they are useful in the following situations:
- To provide a more explicit syntax for writing ref-qualified member functions, discussed in Chapter 9.
- For member function templates where the type of the explicit object parameter is a template type parameter. This can be useful to avoid code duplication when implementing
constand non-constoverloads of member functions and is discussed in Chapter 12, “Writing Generic Code with Templates.” - To write recursive lambda expressions, explained in Chapter 19, “Function Pointers, Function Objects, and Lambda Expressions.”
UNDERSTANDING OBJECT LIFE CYCLES
Section titled “UNDERSTANDING 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
Section titled “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 a smart pointer, new, or new[]. When an object is created, all its embedded objects are also created. Here is an example:
import std;
class MyClass{ private: std::string m_name;};
int main(){ MyClass obj;}The embedded string object is created at the point where the MyClass object is created in the main() function and is destroyed when its containing object is destroyed.
It is often helpful to give variables initial values as you declare them, as so:
int x { 0 };Similarly, you could give initial values to objects. You can provide this functionality by declaring and writing a special member function 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
Section titled “Writing Constructors”Syntactically, a constructor is specified by a member function 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:
export class SpreadsheetCell{ public: SpreadsheetCell(double initialValue); // Remainder of the class definition omitted for brevity};Just as you must provide implementations for normal member functions, 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
Section titled “Using Constructors”Using the constructor creates an object and initializes its values. You can use constructors with both stack-based and free store-based allocation.
Constructors for Objects on the Stack
Section titled “Constructors for Objects on the Stack”When you allocate a SpreadsheetCell object on the stack, you use the constructor like this:
SpreadsheetCell myCell(5), anotherCell(4);println("cell 1: {}", myCell.getValue());println("cell 2: {}", anotherCell.getValue());Alternatively, you can use the uniform initialization syntax:
SpreadsheetCell myCell { 5 }, anotherCell { 4 };Note that you do not call the SpreadsheetCell constructor explicitly. For example, do not do something as follows:
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 for Objects on the Free Store
Section titled “Constructors for Objects on the Free Store”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 } };// Or// SpreadsheetCell* myCellp{ new SpreadsheetCell(5) };SpreadsheetCell* anotherCellp { nullptr };anotherCellp = new SpreadsheetCell { 4 };// … do something with the cellsdelete 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.
Remember to always initialize pointers, either with a proper pointer or with nullptr.
Providing Multiple Constructors
Section titled “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:
export 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);}Here is some code that uses the two different constructors:
SpreadsheetCell aThirdCell { "test" }; // Uses string-arg ctorSpreadsheetCell aFourthCell { 4.4 }; // Uses double-arg ctorauto aFifthCellp { make_unique<SpreadsheetCell>("5.5") }; // string-arg ctorprintln("aThirdCell: {}", aThirdCell.getValue());println("aFourthCell: {}", aFourthCell.getValue());println("aFifthCellp: {}", aFifthCellp->getValue());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 member functions from within other member functions. 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, all is not lost. C++ does support delegating constructors. These allow you to call other constructors of the same class from inside the ctor-initializer, but for this, you’ll have to wait until later in this chapter after the introduction of ctor-initializers.
Default Constructors
Section titled “Default Constructors”A default constructor is a constructor that requires no arguments. It is also called a zero-argument constructor.
When You Need a Default Constructor
Section titled “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 constructorSpreadsheetCell* myCellp { new SpreadsheetCell[10] }; // Also FAILSYou can circumvent this restriction 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.
How to Write a Default Constructor
Section titled “How to Write a Default Constructor”Here is part of the SpreadsheetCell class definition with a default constructor:
export 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(){ m_value = 0;}If you use an in-class member initializer for m_value, 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);println("cell 1: {}", myCell.getValue());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.println("cell 1: {}", myCell.getValue());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!
Of course, instead of using function-call-style parentheses, you can use the uniform initialization syntax as follows:
SpreadsheetCell myCell { }; // Calls the default constructor.When creating an object on the stack with a default constructor, either use curly brackets for the uniform initialization syntax or omit any parentheses.
For free store-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 };// Or// SpreadsheetCell* myCellp { new SpreadsheetCell() };// … use myCellpdelete myCellp; myCellp = nullptr;Compiler-Generated Default Constructor
Section titled “Compiler-Generated Default Constructor”The first SpreadsheetCell class definition in this chapter looked like this:
export class SpreadsheetCell{ public: void setValue(double value); double getValue() const; private: double m_value;};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.
export 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 any constructor yourself, the compiler no longer generates a default constructor for you.
Explicitly Defaulted Default Constructors
Section titled “Explicitly Defaulted Default Constructors”Before C++11, 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 own empty default constructor as shown earlier.
To avoid having to write empty default constructors manually, C++ supports the concept of explicitly defaulted default constructors. This allows you to write the class definition as follows without having to provide an empty implementation for the default constructor:
export 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 because one is explicitly defaulted using the default keyword. You are free to put the = default either directly in the class definition or in an implementation file.
Explicitly Deleted Default Constructors
Section titled “Explicitly Deleted Default Constructors”The opposite of explicitly defaulted default constructors is also possible and is called explicitly deleted default constructors. For example, you can define a class with only static member functions (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.
export class MyClass{ public: MyClass() = delete;};Constructor Initializers aka Ctor-Initializers
Section titled “Constructor Initializers aka Ctor-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) : m_value { initialValue }{}As you can see, the ctor-initializer appears syntactically between the constructor parameter 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, discussed later in this chapter.
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 that 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 a data member an object of a class without a default constructor, you must use the ctor-initializer to properly construct that object. For example, take the following SpreadsheetCell class:
export class SpreadsheetCell{ public: SpreadsheetCell(double d);};This class has only one 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 m_cell;};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 m_cell data member of SomeClass because it does not have a default constructor.
You must initialize the m_cell data member in the ctor-initializer as follows:
SomeClass::SomeClass() : m_cell { 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, and once created, a reference cannot be changed to refer to something else. |
| 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, and you must tell it explicitly which constructor to call. |
| 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 m_value { 0 };};
Foo::Foo(double value) : m_value { value }{ println("Foo::m_value = {}", m_value);}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 m_value { 0 }; Foo m_foo;};Its constructor could be implemented as follows:
MyClass::MyClass(double value) : m_value { value }, m_foo { m_value }{ println("MyClass::m_value = {}", m_value);}The ctor-initializer first stores the given value in m_value and then calls the Foo constructor with m_value as argument. You can create an instance of MyClass as follows:
MyClass instance { 1.2 };Here is the output of the program:
Foo::m_value = 1.2MyClass::m_value = 1.2So, everything looks fine. Now make one tiny change to the MyClass definition; just reverse the order of the m_value and m_foo data members. Nothing else is changed.
class MyClass{ public: MyClass(double value); private: Foo m_foo; double m_value { 0 };};The output of the program now depends on your system. It could, for example, be as follows:
Foo::m_value = -9.255963134931783e+61MyClass::m_value = 1.2This is far from what you expected. You might assume, based on your ctor-initializer, that m_value is initialized before using m_value 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 m_value.
Note that some compilers issue a warning when the order in the ctor-initializer does not match the order in the class definition.
For this example, there is an easy fix. Don’t pass m_value to the Foo constructor, but simply pass the value parameter:
MyClass::MyClass(double value) : m_value { value }, m_foo { value }{ println("MyClass::m_value = {}", m_value);}Ctor-initializers initialize data members in their declared order in the class definition, not their order in the ctor-initializer list.
Copy Constructors
Section titled “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. Here is the declaration for a copy constructor in the SpreadsheetCell class:
export class SpreadsheetCell{ public: SpreadsheetCell(const SpreadsheetCell& src); // Remainder of the class definition omitted for brevity};The copy constructor takes a reference-to-const to the source object. Like other constructors, it does not return a value. The copy constructor should copy all the data members from the source object. Technically, of course, you can do whatever you want in a 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 an example implementation of the SpreadsheetCell copy constructor. Note the use of the ctor-initializer.
SpreadsheetCell::SpreadsheetCell(const SpreadsheetCell& src) : m_value { src.m_value }{}If you don’t write a copy constructor yourself, 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. Given a set of data members, called m1, m2, … mn, this 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
Section titled “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 receives a copy of the value or object. Thus, whenever you pass an object to a function, the compiler calls the copy constructor of the new object to initialize it. For example, suppose you have the following printString() function accepting an std::string parameter by value:
void printString(string value){ println("{}", value);}Recall that std::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 value 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 value object in printString() with name as its argument:
string name { "heading one" };printString(name); // Copies nameWhen the printString() member function finishes, value 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 references-to-const, discussed in an upcoming section.
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
Section titled “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 myCell1Passing Objects by Reference
Section titled “Passing Objects by Reference”To avoid copying objects when you pass them to functions, you should declare that the function 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 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 reference-to-const and has been done in examples throughout this book.
Note that the SpreadsheetCell class has a number of member functions 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 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 as reference-to-const.
The doubleToString() member function of the SpreadsheetCell class always returns a string by value because the implementation of the member function creates a local string object that at the end of the member function is returned to the caller. Returning a reference to this string wouldn’t work because the string to which it refers to will be destroyed when the function exits.
Explicitly Defaulted and Deleted Copy Constructors
Section titled “Explicitly Defaulted and Deleted Copy Constructors”Just as you can explicitly default or delete a compiler-generated default constructor for a class, you can also 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
Section titled “Initializer-List Constructors”An initializer-list constructor is a constructor with an std::initializer_list<T> (see Chapter 1) as the first parameter and without any additional parameters or with additional parameters having default values. The initializer_list<T> class template is defined in <initializer_list>. The following class demonstrates its use. The class accepts only an initializer_list<double> with an even number of elements; otherwise, it throws an exception. Chapter 1 introduces exceptions.
class EvenSequence{ public: EvenSequence(initializer_list<double> values) { if (values.size() % 2 != 0) { throw invalid_argument { "initializer_list should " "contain even number of elements." }; } m_sequence.reserve(values.size()); for (const auto& value : values) { m_sequence.push_back(value); } }
void print() const { for (const auto& value : m_sequence) { std::print("{}, ", value); } println(""); } private: vector<double> m_sequence;};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 an initializer-list with the size() member function.
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() member function of vector. The different member functions of vector, including assign(), are discussed in detail in Chapter 18, “Standard Library Containers.” As a sneak preview, to give you an idea of the power of a vector, here is the initializer-list constructor using assign():
EvenSequence(initializer_list<double> values){ if (values.size() % 2 != 0) { throw invalid_argument { "initializer_list should " "contain even number of elements." }; } m_sequence.assign(values);}EvenSequence objects can be constructed as follows:
try { EvenSequence p1 { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0 }; p1.print();
EvenSequence p2 { 1.0, 2.0, 3.0 };} catch (const invalid_argument& e) { println("{}", e.what());}The construction of p2 throws an exception because it has an odd number of elements in the initializer-list.
The Standard Library has full support for initializer-list constructors. For example, the std::vector container can be initialized with an initializer-list:
vector<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:
vector<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
Section titled “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. The 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 delegating 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 delegating back to the first one. The behavior of such code is undefined by the standard and depends on your compiler.
Converting Constructors and Explicit Constructors
Section titled “Converting Constructors and Explicit Constructors”The current set of constructors for SpreadsheetCell is as follows:
export class SpreadsheetCell{ public: SpreadsheetCell() = default; SpreadsheetCell(double initialValue); SpreadsheetCell(std::string_view initialValue); SpreadsheetCell(const SpreadsheetCell& src); // Remainder omitted for brevity};The single-parameter double and string_view constructors can be used to convert a double or a string_view into a SpreadsheetCell. Such constructors are called converting constructors. The compiler can use such constructors to perform implicit conversions for you. Here’s an example:
SpreadsheetCell myCell { 4 };myCell = 5;myCell = "6"sv; // A string_view literal (see Chapter 2).This might not always be the behavior you want. You can prevent the compiler from doing such implicit conversions by marking constructors as explicit. The explicit keyword goes only in the class definition. Here’s an example:
export class SpreadsheetCell{ public: SpreadsheetCell() = default; SpreadsheetCell(double initialValue); explicit SpreadsheetCell(std::string_view initialValue); SpreadsheetCell(const SpreadsheetCell& src); // Remainder omitted for brevity};With this change, a line as follows no longer compiles:
myCell = "6"sv; // A string_view literal (see Chapter 2).Prior to C++11, converting constructors could have only a single parameter, as in the SpreadsheetCell example. Since C++11, converting constructors can have multiple parameters because of support for list initialization. Let’s look at an example. Suppose you have the following class:
class MyClass{ public: MyClass(int) { } MyClass(int, int) { }};This class has two constructors, and since C++11, both are converting constructors. The following example shows that the compiler automatically converts arguments such as 1, {1}, and {1,2}, to instances of MyClass using these converting constructors:
void process(const MyClass& c) { }
int main(){ process(1); process({ 1 }); process({ 1, 2 });}To prevent these implicit conversions, both converting constructors can be marked as explicit:
class MyClass{ public: explicit MyClass(int) { } explicit MyClass(int, int) { }};With this change, you have to perform these conversions explicitly; here’s an example:
process(MyClass{ 1 });process(MyClass{ 1, 2 });It is possible to pass a Boolean argument to explicit to turn it into a conditional explicit. The syntax is as follows:
explicit(true) MyClass(int);Of course, just writing explicit(true) is equivalent to explicit, but it becomes more useful in the context of generic template code using type traits. With type traits you can query certain properties of given types, such as whether a certain type is convertible to another type. The result of such a type trait can be used as argument to explicit(). Type traits allow for writing advanced generic code and are discussed in Chapter 26, “Advanced Templates.”
Summary of Compiler-Generated Constructors
Section titled “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 a; As a copy: SpreadsheetCell b{a}; |
| A default constructor only | A copy constructor | With no arguments: SpreadsheetCell a; As a copy: SpreadsheetCell b{a}; |
| 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- or multi-argument non-copy constructor only | A copy constructor | With arguments: SpreadsheetCell a{6}; As a copy: SpreadsheetCell b{a}; |
| A default constructor as well as a single- or multi-argument non-copy constructor | A copy constructor | With no arguments: SpreadsheetCell a; With arguments: SpreadsheetCell b{5}; As a copy: SpreadsheetCell c{a}; |
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
Section titled “Object Destruction”When an object is destroyed, two events occur: the object’s destructor member function 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. A destructor of a class is a member function with as name the name of the class prefixed with a tilde (˜). A destructor does not return anything and does not have any parameters. Here is an example of a destructor that simply writes something to standard output:
export class SpreadsheetCell{ public: ˜SpreadsheetCell(); // Destructor. // Remainder of the class definition omitted for brevity};
SpreadsheetCell::˜SpreadsheetCell(){ println("Destructor called.");}Objects on the stack are destroyed when they go out of scope, which means whenever the current function 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.
println("myCell: {}", myCell.getValue());} // 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 myCell2This 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 order in the class.
Objects allocated on the free store without the help of smart pointers are not destroyed automatically. You must call delete on the object pointer to call its destructor and free its memory. The following program shows this behavior.
Do not write programs like the next example where cellPtr2 is not deleted. Make sure you always free dynamically allocated memory by calling delete or delete[] depending on whether the memory was allocated using new or new[]. Or better yet, use smart pointers as discussed earlier!
int main(){ SpreadsheetCell* cellPtr1 { new SpreadsheetCell { 5 } }; SpreadsheetCell* cellPtr2 { new SpreadsheetCell { 6 } }; println("cellPtr1: {}", cellPtr1->getValue()); delete cellPtr1; // Destroys cellPtr1 cellPtr1 = nullptr;} // cellPtr2 is NOT destroyed because delete was not called on it.Assigning to Objects
Section titled “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” occurs only 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 member function in every class to perform assignment. This member function is called the assignment operator. Its name is operator= because it is actually an overload 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
Section titled “Declaring an Assignment Operator”Here is the assignment operator for the SpreadsheetCell class:
export class SpreadsheetCell{ public: SpreadsheetCell& operator=(const SpreadsheetCell& rhs); // Remainder of the class definition omitted for brevity};The assignment operator usually takes a reference-to-const 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 equal sign, but you are of course free to call it whatever you want. The object on which the assignment operator is called is the left-hand side of the equal 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” argument. Next, the assignment operator for myCell is called. However, its argument is not anotherCell; its right-hand side is the result of the assignment of aThirdCell to anotherCell. The equal sign is simply just shorthand for what is really a member function call. When you look at the line in its full functional syntax shown here, 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 a reference to anotherCell itself, so it can serve as the source for the assignment to myCell.
You could actually declare the assignment operator to return whatever type you wanted, including void. However, you should always return a reference to the object on which it is called because that’s what clients expect.
Defining an Assignment Operator
Section titled “Defining an Assignment Operator”The implementation of the assignment operator is similar to that of a copy constructor but 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 resources, such as 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-assignmentYour 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, the first thing assignment operators usually do is check for self-assignment and return immediately if that’s the case.
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 equal sign are the same. One way to tell whether 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 member function 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 as follows, and the self-assignment case is no exception:
return *this; }this is a pointer to the object on which the member function executes, so *this is the object itself. The compiler returns a reference to the object to match the declared return type. Now, if it is not self-assignment, you have to do an assignment to every member:
m_value = rhs.m_value; return *this;}Here the member function copies the values, and finally, it returns *this, as explained earlier.
Astute readers will notice there’s some code duplication between the copy assignment operator and the copy constructor; they both need to copy all data members. Chapter 9 introduces the copy-and-swap idiom to prevent such code duplication.
If your class requires special handling for copy operations, always implement both the copy constructor and the copy assignment operator.
Explicitly Defaulted and Deleted Assignment Operator
Section titled “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
Section titled “Compiler-Generated Copy Constructor and Copy Assignment Operator”C++11 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 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
Section titled “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. Now consider the following:
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 anotherCellHere, anotherCell has already been constructed, so the compiler calls operator=.
Objects as Return Values
Section titled “Objects as Return Values”When you return objects from functions, it is sometimes difficult to see exactly what copying and assigning is happening. For example, the implementation of SpreadsheetCell::getString() looks like this:
string SpreadsheetCell::getString() const{ return doubleToString(m_value);}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 could invoke the copy constructor and the assignment operator (for two different objects).
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 or move assignment operator instead of a copy constructor or copy assignment operator to return the string from getString(). This can be more efficient in certain cases and is discussed in Chapter 9. However, even better, compilers are free to (and often even required to) implement copy elision to optimize away costly copy operations or move operations when returning values; see Chapter 1.
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 your code with a debugger.
Copy Constructors and Object Members
Section titled “Copy Constructors and Object Members”You should also note the difference between assignment operator 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 the SpreadsheetCell copy constructor like this:
SpreadsheetCell::SpreadsheetCell(const SpreadsheetCell& src){ m_value = src.m_value;}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.
If you write the copy constructor as follows, then m_value is initialized using the copy constructor:
SpreadsheetCell::SpreadsheetCell(const SpreadsheetCell& src) : m_value { src.m_value }{}SUMMARY
Section titled “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 member functions 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 it 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.
EXERCISES
Section titled “EXERCISES”By solving the following exercises, you can practice the material discussed in this chapter. Solutions to all exercises are available with the code download on the book’s website at www.wiley.com/go/proc++6e. However, if you are stuck on an exercise, first reread parts of this chapter to try to find an answer yourself before looking at the solution from the website.
-
Exercise 8-1: Implement a
Personclass storing a first and last name as data members. Add a single constructor accepting two parameters, the first and last name. Provide appropriate getters and setters. Write a smallmain()function to test your implementation by creating aPersonobject on the stack and on the free store. -
Exercise 8-2: With the set of member functions implemented in Exercise 8-1, the following line of code does not compile:
Person persons[3];Can you explain why this does not compile? Modify the implementation of your
Personclass to make this work. -
Exercise 8-3: Add the following member functions to your
Personclass implementation: a copy constructor, a copy assignment operator, and a destructor. In all of these member functions, implement what you think is necessary, and additionally, output a line of text to the console so you can trace when they are executed. Modify yourmain()function to test these new member functions. Note: technically, these new member functions are not strictly required for thisPersonclass, because the compiler-generated versions are good enough, but this exercise is to practice writing them. -
Exercise 8-4: Remove the copy constructor, copy assignment operator, and destructor from your
Personclass, because the default compiler-generated versions are exactly what you need for this simple class. Next, add a new data member to store the initials of a person, and provide a getter and setter. Add a new constructor that accepts three parameters, a first and last name, and a person’s initials. Modify the original two-parameter constructor to automatically generate initials for a given first and last name, and delegate the actual construction work to the new three-parameter constructor. Test this new functionality in yourmain()function.