9
Mastering Classes and Objects

FRIENDS

C++ allows classes to declare that other classes, member functions of other classes, or non-member functions are friends, and can access protected and private data members and methods. For example, suppose you have two classes called Foo and Bar. You can specify that the Bar class is a friend of Foo as follows:

class Foo
{
    friend class Bar;
    // …
}; 

Now all the methods of Bar can access the private and protected data members and methods of Foo.

If you only want to make a specific method of Bar a friend, you can do that as well. Suppose the Bar class has a method processFoo(const Foo& foo). The following syntax is used to make this method a friend of Foo:

class Foo
{
    friend void Bar::processFoo(const Foo& foo);
    // …
}; 

Standalone functions can also be friends of classes. You might, for example, want to write a function that dumps all data of a Foo object to the console. You might want this function to be outside the Foo class to model an external audit, but the function should be able to access the internal data members of the object in order to check it properly. Here is the Foo class definition with a friend dumpFoo() function:

class Foo
{
    friend void dumpFoo(const Foo& foo);
    // …
}; 

The friend declaration in the class serves as the function’s prototype. There’s no need to write the prototype elsewhere (although it’s harmless to do so).

Here is the function definition:

void dumpFoo(const Foo& foo)
{
    // Dump all data of foo to the console, including
    // private and protected data members.
}

You write this function just like any other function, except that you can directly access private and protected data members of Foo. You don’t repeat the friend keyword in the function definition.

Note that a class needs to know which other classes, methods, or functions want to be its friends; a class, method, or function cannot declare itself to be a friend of some other class and access the non-public names of that class.

friend classes and methods are easy to abuse; they allow you to violate the principle of encapsulation by exposing internals of your class to other classes or functions. Thus, you should use them only in limited circumstances. Some use cases are shown throughout this chapter.

DYNAMIC MEMORY ALLOCATION IN OBJECTS

Sometimes you don’t know how much memory you will need before your program actually runs. As you read in Chapter 7, the solution is to dynamically allocate as much space as you need during program execution. Classes are no exception. Sometimes you don’t know how much memory an object will need when you write the class. In that case, the object should dynamically allocate memory. Dynamically allocated memory in objects provides several challenges, including freeing the memory, handling object copying, and handling object assignment.

The Spreadsheet Class

Chapter 8 introduces the SpreadsheetCell class. This chapter moves on to write the Spreadsheet class. As with the SpreadsheetCell class, the Spreadsheet class evolves throughout this chapter. Thus, the various attempts do not always illustrate the best way to do every aspect of class writing. To start, a Spreadsheet is simply a two-dimensional array of SpreadsheetCells, with methods to set and retrieve cells at specific locations in the Spreadsheet. Although most spreadsheet applications use letters in one direction and numbers in the other to refer to cells, this Spreadsheet uses numbers in both directions. Here is a first attempt at a class definition for a simple Spreadsheet class:

#include <cstddef>
#include "SpreadsheetCell.h"

class Spreadsheet
{
    public:
        Spreadsheet(size_t width, size_t height);
        void setCellAt(size_t x, size_t y, const SpreadsheetCell& cell);
        SpreadsheetCell& getCellAt(size_t x, size_t y);
    private:
        bool inRange(size_t value, size_t upper) const;
        size_t mWidth = 0;
        size_t mHeight = 0;
        SpreadsheetCell** mCells = nullptr;
};

Note that the Spreadsheet class does not contain a standard two-dimensional array of SpreadsheetCells. Instead, it contains a SpreadsheetCell**. This is because each Spreadsheet object might have different dimensions, so the constructor of the class must dynamically allocate the two-dimensional array based on the client-specified height and width. In order to allocate dynamically a two-dimensional array, you need to write the following code. Note that in C++, unlike in Java, it’s not possible to simply write new SpreadsheetCell[mWidth][mHeight].

Spreadsheet::Spreadsheet(size_t width, size_t height)
    : mWidth(width), mHeight(height)
{
    mCells = new SpreadsheetCell*[mWidth];
    for (size_t i = 0; i < mWidth; i++) {
        mCells[i] = new SpreadsheetCell[mHeight];
    }
}

The resulting memory for a Spreadsheet called s1 on the stack with width 4 and height 3 is shown in Figure 9-1.

Representation of resulting memory for a Spreadsheet called s1 on the stack with width four and height three.

FIGURE 9-1

The implementations of the set and retrieval methods are straightforward:

void Spreadsheet::setCellAt(size_t x, size_t y, const SpreadsheetCell& cell)
{
    if (!inRange(x, mWidth) || !inRange(y, mHeight)) {
        throw std::out_of_range("");
    }
    mCells[x][y] = cell;
}

SpreadsheetCell& Spreadsheet::getCellAt(size_t x, size_t y)
{
    if (!inRange(x, mWidth) || !inRange(y, mHeight)) {
        throw std::out_of_range("");
    }
    return mCells[x][y];
}

Note that these two methods use a helper method inRange() to check that x and y represent valid coordinates in the spreadsheet. Attempting to access an array element at an out-of-range index will cause the program to malfunction. This example uses exceptions, which are mentioned in Chapter 1 and described in detail in Chapter 14.

If you look at the setCellAt() and getCellAt() methods, you see there is some clear code duplication. Chapter 6 explains that code duplication should be avoided at all costs. So, let’s follow that guideline. Instead of a helper method called inRange(), the following verifyCoordinate() method is defined for the class:

void verifyCoordinate(size_t x, size_t y) const;

The implementation checks the given coordinate and throws an exception if the coordinate is invalid:

void Spreadsheet::verifyCoordinate(size_t x, size_t y) const
{
    if (x >= mWidth || y >= mHeight) {
        throw std::out_of_range("");
    }
}

The setCellAt() and getCellAt() methods can now be simplified:

void Spreadsheet::setCellAt(size_t x, size_t y, const SpreadsheetCell& cell)
{
    verifyCoordinate(x, y);
    mCells[x][y] = cell;
}

SpreadsheetCell& Spreadsheet::getCellAt(size_t x, size_t y)
{
    verifyCoordinate(x, y);
    return mCells[x][y];
}

Freeing Memory with Destructors

Whenever you are finished with dynamically allocated memory, you should free it. If you dynamically allocate memory in an object, the place to free that memory is in the destructor. The compiler guarantees that the destructor is called when the object is destroyed. Here is the Spreadsheet class definition with a destructor:

class Spreadsheet
{
    public:
        Spreadsheet(size_t width, size_t height);
        ~Spreadsheet();
        // Code omitted for brevity
};

The destructor has the same name as the name of the class (and of the constructors), preceded by a tilde (~). The destructor takes no arguments, and there can only be one of them. Destructors are implicitly marked as noexcept, since they should not throw any exceptions.

Here is the implementation of the Spreadsheet class destructor:

Spreadsheet::~Spreadsheet()
{
    for (size_t i = 0; i < mWidth; i++) {
        delete [] mCells[i];
    }
    delete [] mCells;
    mCells = nullptr;
}

This destructor frees the memory that was allocated in the constructor. However, no rule requires you to free memory in the destructor. You can write whatever code you want in the destructor, but it is a good idea to use it only for freeing memory or disposing of other resources.

Handling Copying and Assignment

Recall from Chapter 8 that if you don’t write a copy constructor and an assignment operator yourself, C++ writes them for you. These compiler-generated methods recursively call the copy constructor or assignment operator on object data members. However, for primitives, such as int, double, and pointers, they provide shallow or bitwise copying or assignment: they just copy or assign the data members from the source object directly to the destination object. That presents problems when you dynamically allocate memory in your object. For example, the following code copies the spreadsheet s1 to initialize s when s1 is passed to the printSpreadsheet() function:

#include "Spreadsheet.h"

void printSpreadsheet(Spreadsheet s)
{
   // Code omitted for brevity.
}

int main()
{
    Spreadsheet s1(4, 3);
    printSpreadsheet(s1);
    return 0;
}

The Spreadsheet contains one pointer variable: mCells. A shallow copy of a spreadsheet gives the destination object a copy of the mCells pointer, but not a copy of the underlying data. Thus, you end up with a situation where both s and s1 have a pointer to the same data, as shown in Figure 9-2.

Representation of a situation where both s and s1 have a pointer to the same data.

FIGURE 9-2

If s changes something to which mCells points, that change shows up in s1 too. Even worse, when the printSpreadsheet() function exits, s’s destructor is called, which frees the memory pointed to by mCells. That leaves the situation shown in Figure 9-3.

Representation of memory pointed to by mCells is freed.

FIGURE 9-3

Now s1 has a pointer that no longer points to valid memory. This is called a dangling pointer.

Unbelievably, the problem is even worse with assignment. Suppose that you have the following code:

Spreadsheet s1(2, 2), s2(4, 3);
s1 = s2;

After the first line, when both objects are constructed, you have the memory layout shown in Figure 9-4.

Representation of a memory layout whenboth objects are constructed after the first line.

FIGURE 9-4

After the assignment statement, you have the layout shown in Figure 9-5.

Representation of a memory layout.

FIGURE 9-5

Now, not only do the mCells pointers in s1 and s2 point to the same memory, but you have also orphaned the memory to which mCells in s1 previously pointed. This is called a memory leak. That is why in assignment operators you must do a deep copy.

As you can see, relying on C++’s default copy constructor and default assignment operator is not always a good idea.

The Spreadsheet Copy Constructor

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

class Spreadsheet
{
    public:
        Spreadsheet(const Spreadsheet& src);
        // Code omitted for brevity
};

The definition is as follows:

Spreadsheet::Spreadsheet(const Spreadsheet& src)
    : Spreadsheet(src.mWidth, src.mHeight)
{
    for (size_t i = 0; i < mWidth; i++) {
        for (size_t j = 0; j < mHeight; j++) {
            mCells[i][j] = src.mCells[i][j];
        }
    }
}

Note the use of a delegating constructor. The ctor-initializer of this copy constructor delegates first to the non-copy constructor to allocate the proper amount of memory. The body of the copy constructor then copies the actual values. Together, this process implements a deep copy of the mCells dynamically allocated two-dimensional array.

There is no need to delete the existing mCells because this is a copy constructor and therefore there is not an existing mCells yet in this object.

The Spreadsheet Assignment Operator

The following shows the Spreadsheet class definition with an assignment operator:

class Spreadsheet
{
    public:
        Spreadsheet& operator=(const Spreadsheet& rhs);
        // Code omitted for brevity
};

A naïve implementation could be as follows:

Spreadsheet& Spreadsheet::operator=(const Spreadsheet& rhs)
{
    // Check for self-assignment
    if (this == &rhs) {
        return *this;
    }

    // Free the old memory
    for (size_t i = 0; i < mWidth; i++) {
        delete[] mCells[i];
    }
    delete[] mCells;
    mCells = nullptr;

    // Allocate new memory
    mWidth = rhs.mWidth;
    mHeight = rhs.mHeight;

    mCells = new SpreadsheetCell*[mWidth];
    for (size_t i = 0; i < mWidth; i++) {
        mCells[i] = new SpreadsheetCell[mHeight];
    }

    // Copy the data
    for (size_t i = 0; i < mWidth; i++) {
        for (size_t j = 0; j < mHeight; j++) {
            mCells[i][j] = rhs.mCells[i][j];
        }
    }

    return *this;
}

The code first checks for self-assignment, then frees the current memory of the this object, followed by allocating new memory, and finally copying the individual elements. There is a lot going on in this method, and a lot can go wrong! It is possible that the this object gets into an invalid state. For example, suppose that the memory is successfully freed, that mWidth and mHeight are properly set, but that an exception is thrown in the loop that is allocating the memory. When that happens, execution of the remainder of the method is skipped, and the method is exited. Now the Spreadsheet instance is corrupt; its mWidth and mHeight data members state a certain size, but the mCells data member does not have the right amount of memory. Basically, this code is not exception safe!

What we need is an all-or-nothing mechanism; either everything succeeds, or the this object remains untouched. To implement such an exception-safe assignment operator, the copy-and-swap idiom is recommended. For this, a non-member swap() function is implemented as a friend of the Spreadsheet class. Instead of a non-member swap() function, you could also add a swap() method to the class. However, it’s recommended practice to implement swap() as a non-member function, so that it can also be used by various Standard Library algorithms. Here is the definition of the Spreadsheet class with an assignment operator and the swap() function:

class Spreadsheet
{
    public:
        Spreadsheet& operator=(const Spreadsheet& rhs);
        friend void swap(Spreadsheet& first, Spreadsheet& second) noexcept;
        // Code omitted for brevity
};

A requirement for implementing the exception-safe copy-and-swap idiom is that the swap() function never throws any exceptions, so it is marked as noexcept. The implementation of the swap() function swaps each data member using the std::swap() utility function provided by the Standard Library in the <utility> header file:

void swap(Spreadsheet& first, Spreadsheet& second) noexcept
{
    using std::swap;

    swap(first.mWidth, second.mWidth);
    swap(first.mHeight, second.mHeight);
    swap(first.mCells, second.mCells);
}

Now that we have this exception-safe swap() function, it can be used to implement the assignment operator:

Spreadsheet& Spreadsheet::operator=(const Spreadsheet& rhs)
{
    // Check for self-assignment
    if (this == &rhs) {
        return *this;
    }

    Spreadsheet temp(rhs); // Do all the work in a temporary instance
    swap(*this, temp); // Commit the work with only non-throwing operations
    return *this;
}

The implementation uses the copy-and-swap idiom. For efficiency, but sometimes also for correctness, the first line of code in an assignment operator usually checks for self-assignment. Next, a copy of the right-hand-side is made, called temp. Then *this is swapped with this copy. This pattern is the recommended way of implementing your assignment operators because it guarantees strong exception safety. This means that if any exception occurs, then the state of the current Spreadsheet object remains unchanged. This is implemented in three phases:

  • The first phase makes a temporary copy. This does not modify the state of the current Spreadsheet object, and so there is no problem if an exception is thrown during this phase.
  • The second phase uses the swap() function to swap the created temporary copy with the current object. The swap() function shall never throw exceptions.
  • The third phase is the destruction of the temporary object, which now contains the original object (because of the swap), to clean up any memory.

Disallowing Assignment and Pass-By-Value

Sometimes when you dynamically allocate memory in your class, it’s easiest just to prevent anyone from copying or assigning to your objects. You can do this by explicitly deleting your operator= and copy constructor. That way, if anyone tries to pass the object by value, return it from a function or method, or assign to it, the compiler will complain. Here is a Spreadsheet class definition that prevents assignment and pass-by-value:

class Spreadsheet
{
    public:
        Spreadsheet(size_t width, size_t height);
        Spreadsheet(const Spreadsheet& src) = delete;
        ~Spreadsheet();
        Spreadsheet& operator=(const Spreadsheet& rhs) = delete;
        // Code omitted for brevity
};

You don’t provide implementations for deleted methods. The linker will never look for them because the compiler won’t allow code to call them. When you write code to copy or assign to a Spreadsheet object, the compiler will complain with a message like this:

'Spreadsheet &Spreadsheet::operator =(const Spreadsheet &)' : attempting to reference a deleted function 

Handling Moving with Move Semantics

Move semantics for objects requires a move constructor and a move assignment operator. These can be used by the compiler when the source object is a temporary object that will be destroyed after the operation is finished. Both the move constructor and the move assignment operator move the data members from the source object to the new object, leaving the source object in some valid but otherwise indeterminate state. Often, data members of the source object are reset to null values. This process actually moves ownership of the memory and other resources from one object to another object. They basically do a shallow copy of the member variables, and switch ownership of allocated memory and other resources to prevent dangling pointers or resources, and to prevent memory leaks.

Before you can implement move semantics, you need to learn about rvalues and rvalue references.

Rvalue References

In C++, an lvalue is something of which you can take an address; for example, a named variable. The name comes from the fact that lvalues can appear on the left-hand side of an assignment. An rvalue, on the other hand, is anything that is not an lvalue, such as a literal, or a temporary object or value. Typically, an rvalue is on the right-hand side of an assignment operator. For example, take the following statement:

int a = 4 * 2;

In this statement, a is an lvalue, it has a name and you can take the address of it with &a. The result of the expression 4 * 2 on the other hand is an rvalue. It is a temporary value that is destroyed when the statement finishes execution. In this example, a copy of this temporary value is stored in the variable with name a.

An rvalue reference is a reference to an rvalue. In particular, it is a concept that is applied when the rvalue is a temporary object. The purpose of an rvalue reference is to make it possible for a particular function to be chosen when a temporary object is involved. The consequence of this is that certain operations that normally involve copying large values can be implemented by copying pointers to those values, knowing the temporary object will be destroyed.

A function can specify an rvalue reference parameter by using && as part of the parameter specification; for example, type&& name. Normally, a temporary object will be seen as a const type&, but when there is a function overload that uses an rvalue reference, a temporary object can be resolved to that overload. The following example demonstrates this. The code first defines two handleMessage() functions, one accepting an lvalue reference and one accepting an rvalue reference:

// lvalue reference parameter
void handleMessage(std::string& message)
{
    cout << "handleMessage with lvalue reference: " << message << endl;
}

// rvalue reference parameter
void handleMessage(std::string&& message)
{
    cout << "handleMessage with rvalue reference: " << message << endl;
}

You can call handleMessage() with a named variable as argument:

std::string a = "Hello ";
std::string b = "World";
handleMessage(a);             // Calls handleMessage(string& value)

Because a is a named variable, the handleMessage() function accepting an lvalue reference is called. Any changes handleMessage() does through its reference parameter will change the value of a.

You can also call handleMessage() with an expression as argument:

handleMessage(a + b);         // Calls handleMessage(string&& value)

The handleMessage() function accepting an lvalue reference cannot be used, because the expression a + b results in a temporary, which is not an lvalue. In this case the rvalue reference version is called. Because the argument is a temporary, any changes handleMessage() does through its reference parameter will be lost after the call returns.

A literal can also be used as argument to handleMessage(). This also triggers a call to the rvalue reference version because a literal cannot be an lvalue (though a literal can be passed as argument to a const reference parameter).

handleMessage("Hello World"); // Calls handleMessage(string&& value)

If you remove the handleMessage() function accepting an lvalue reference, calling handleMessage() with a named variable like handleMessage(b) will result in a compilation error because an rvalue reference parameter (string&& message) will never be bound to an lvalue (b). You can force the compiler to call the rvalue reference version of handleMessage() by using std::move(), which casts an lvalue into an rvalue as follows:

handleMessage(std::move(b));  // Calls handleMessage(string&& value)

As said before, but it’s worth repeating, a named variable is an lvalue. So, inside the handleMessage() function, the rvalue reference message parameter itself is an lvalue because it has a name! If you want to forward this rvalue reference parameter to another function as an rvalue, then you need to use std::move() to cast the lvalue to an rvalue. For example, suppose you add the following function with an rvalue reference parameter:

void helper(std::string&& message)
{
}

Calling it as follows does not compile:

void handleMessage(std::string&& message)
{
    helper(message);
}

The helper() function needs an rvalue reference, while handleMessage() passes message, which has a name, so it’s an lvalue, causing a compilation error. The correct way is to use std::move():

void handleMessage(std::string&& message)
{
    helper(std::move(message));
}

Rvalue references are not limited to parameters of functions. You can declare a variable of an rvalue reference type, and assign to it, although this usage is uncommon. Consider the following code, which is illegal in C++:

int& i = 2;       // Invalid: reference to a constant
int a = 2, b = 3;
int& j = a + b;   // Invalid: reference to a temporary

Using rvalue references, the following is perfectly legal:

int&& i = 2;
int a = 2, b = 3;
int&& j = a + b;

Stand-alone rvalue references, as in the preceding example, are rarely used in this way.

Implementing Move Semantics

Move semantics is implemented by using rvalue references. To add move semantics to a class, you need to implement a move constructor and a move assignment operator. Move constructors and move assignment operators should be marked with the noexcept qualifier to tell the compiler that they don’t throw any exceptions. This is particularly important for compatibility with the Standard Library, as fully compliant implementations of, for example, the Standard Library containers will only move stored objects if, having move semantics implemented, they also guarantee not to throw. Following is the Spreadsheet class definition with a move constructor and move assignment operator. Two helper methods are introduced as well: cleanup(), which is used from the destructor and the move assignment operator, and moveFrom(), which moves the member variables from a source to a destination, and then resets the source object.

class Spreadsheet
{
    public:
        Spreadsheet(Spreadsheet&& src) noexcept; // Move constructor
        Spreadsheet& operator=(Spreadsheet&& rhs) noexcept; // Move assign
        // Remaining code omitted for brevity
    private:
        void cleanup() noexcept;
        void moveFrom(Spreadsheet& src) noexcept;
        // Remaining code omitted for brevity
};

The implementations are as follows:

void Spreadsheet::cleanup() noexcept
{
    for (size_t i = 0; i < mWidth; i++) {
        delete[] mCells[i];
    }
    delete[] mCells;
    mCells = nullptr;
    mWidth = mHeight = 0;
}

void Spreadsheet::moveFrom(Spreadsheet& src) noexcept
{
    // Shallow copy of data
    mWidth = src.mWidth;
    mHeight = src.mHeight;
    mCells = src.mCells;

    // Reset the source object, because ownership has been moved!
    src.mWidth = 0;
    src.mHeight = 0;
    src.mCells = nullptr;
}

// Move constructor
Spreadsheet::Spreadsheet(Spreadsheet&& src) noexcept
{
    moveFrom(src);
}

// Move assignment operator
Spreadsheet& Spreadsheet::operator=(Spreadsheet&& rhs) noexcept
{
    // check for self-assignment
    if (this == &rhs) {
        return *this;
    }

    // free the old memory
    cleanup();

    moveFrom(rhs);

    return *this;
}

Both the move constructor and the move assignment operator are moving ownership of the memory for mCells from the source object to the new object. They reset the mCells pointer of the source object to a null pointer to prevent the destructor of the source object from deallocating that memory because now the new object is the owner of that memory.

Obviously, move semantics is useful only when you know that the source object will be destroyed.

Move constructors and move assignment operators can be explicitly deleted or defaulted, just like normal constructors and copy assignment operators, as explained in Chapter 8.

The compiler automatically generates a default move constructor for a class if and only if the class has no user-declared copy constructor, copy assignment operator, move assignment operator, or destructor. A default move assignment operator is generated for a class if and only if the class has no user-declared copy constructor, move constructor, copy assignment operator, or destructor.

Moving Object Data Members

The moveFrom() method uses direct assignments of the three data members because they are primitive types. If your object has other objects as data members, then you should move these objects using std::move(). Suppose the Spreadsheet class has an std::string data member called mName. The moveFrom() method is then implemented as follows:

void Spreadsheet::moveFrom(Spreadsheet& src) noexcept
{
    // Move object data members
    mName = std::move(src.mName);

    // Move primitives:
    // Shallow copy of data
    mWidth = src.mWidth;
    mHeight = src.mHeight;
    mCells = src.mCells;

    // Reset the source object, because ownership has been moved!
    src.mWidth = 0;
    src.mHeight = 0;
    src.mCells = nullptr;
}

Move Constructor and Move Assignment Operator in Terms of Swap

The previous implementation of the move constructor and the move assignment operator both use the moveFrom() helper method which moves all data members by performing shallow copies. With this implementation, if you add a new data member to the Spreadsheet class, you have to modify both the swap() function and the moveFrom() method. If you forget to update one of them, you introduce a bug. To avoid such bugs, you can write the move constructor and the move assignment operator in terms of a default constructor and the swap() function.

The first thing to do is to add a default constructor to the Spreadsheet class. It doesn’t make sense for users of the class to use this default constructor, so it’s marked as private:

class Spreadsheet
{
    private:
        Spreadsheet() = default;
        // Remaining code omitted for brevity
};

Next, we can remove the cleanup() and moveFrom() helper methods. The code from the cleanup() method is moved to the destructor. The move constructor and move assignment operator can then be implemented as follows:

Spreadsheet::Spreadsheet(Spreadsheet&& src) noexcept
    : Spreadsheet()
{
    swap(*this, src);
}

Spreadsheet& Spreadsheet::operator=(Spreadsheet&& rhs) noexcept
{
    Spreadsheet temp(std::move(rhs));
    swap(*this, temp);
    return *this;
}

The move constructor delegates first to the default constructor. Then, the default constructed *this is swapped with the given source object. The move assignment operator first creates a local Spreadsheet instance that is move-constructed with rhs. Then, *this is swapped with this local move-constructed Spreadsheet instance.

Implementing the move constructor and move assignment operator in terms of the default constructor and the swap() function might be a tiny bit less efficient compared to the earlier implementation using moveFrom(). The advantage however is that it requires less code and it is less likely bugs are introduced when data members are added to the class, because you only have to update your swap() implementation to include the new data members.

Testing the Spreadsheet Move Operations

The Spreadsheet move constructor and move assignment operator can be tested with the following code:

Spreadsheet createObject()
{
    return Spreadsheet(3, 2);
}

int main()
{
    vector<Spreadsheet> vec;
    for (int i = 0; i < 2; ++i) {
        cout << "Iteration " << i << endl;
        vec.push_back(Spreadsheet(100, 100));
        cout << endl;
    }

    Spreadsheet s(2,3);
    s = createObject();

    Spreadsheet s2(5,6);
    s2 = s;
    return 0;
}

Chapter 1 introduces the vector. A vector grows dynamically in size to accommodate new objects. This is done by allocating a bigger chunk of memory and then copying or moving the objects from the old vector to the new and bigger vector. If the compiler finds a move constructor, the objects are moved instead of copied. Because they are moved, there is no need for any deep copying, making it much more efficient.

When you add output statements to all constructors and assignment operators of the Spreadsheet class implemented with the moveFrom() method, the output of the preceding test program can be as follows. This output and the following discussion are based on the Microsoft Visual C++ 2017 compiler. The C++ standard does not specify the initial capacity of a vector or its growth strategy, so the output can be different on different compilers.

Iteration 0
Normal constructor         (1)
Move constructor           (2)

Iteration 1
Normal constructor         (3)
Move constructor           (4)
Move constructor           (5)

Normal constructor         (6)
Normal constructor         (7)
Move assignment operator   (8)
Normal constructor         (9)
Copy assignment operator  (10)
Normal constructor        (11)
Copy constructor          (12)

On the first iteration of the loop, the vector is still empty. Take the following line of code from the loop:

vec.push_back(Spreadsheet(100, 100));

With this line, a new Spreadsheet object is created, invoking the normal constructor (1). The vector resizes itself to make space for the new object being pushed in. The created Spreadsheet object is then moved into the vector, invoking the move constructor (2).

On the second iteration of the loop, a second Spreadsheet object is created with the normal constructor (3). At this point, the vector can hold one element, so it’s again resized to make space for a second object. Because the vector is resized, the previously added elements need to be moved from the old vector to the new and bigger vector. This triggers a call to the move constructor for each previously added element. There is one element in the vector, so the move constructor is called one time (4). Finally, the new Spreadsheet object is moved into the vector with its move constructor (5).

Next, a Spreadsheet object s is created using the normal constructor (6). The createObject() function creates a temporary Spreadsheet object with its normal constructor (7), which is then returned from the function, and assigned to the variable s. Because the temporary object returned from createObject() ceases to exist after the assignment, the compiler invokes the move assignment operator (8) instead of the normal copy assignment operator. Another Spreadsheet object is created, s2, using the normal constructor (9). The assignment s2 = s invokes the copy assignment operator (10) because the right-hand side object is not a temporary object, but a named object. This copy assignment operator creates a temporary copy, which triggers a call to the copy constructor, which first delegates to the normal constructor (11 and 12).

If the Spreadsheet class did not implement move semantics, all the calls to the move constructor and move assignment operator would be replaced with calls to the copy constructor and copy assignment operator. In the previous example, the Spreadsheet objects in the loop have 10,000 (100 x 100) elements. The implementation of the Spreadsheet move constructor and move assignment operator doesn’t require any memory allocation, while the copy constructor and copy assignment operator require 101 allocations each. So, using move semantics can increase performance a lot in certain situations.

Implementing a Swap Function with Move Semantics

As another example where move semantics increases performance, take a swap() function that swaps two objects. The following swapCopy() implementation does not use move semantics:

void swapCopy(T& a, T& b)
{
    T temp(a);
    a = b;
    b = temp;
}

First, a is copied to temp, then b is copied to a, and finally temp is copied to b. This implementation will hurt performance if type T is expensive to copy. With move semantics, the implementation can avoid all copying:

void swapMove(T& a, T& b)
{
    T temp(std::move(a));
    a = std::move(b);
    b = std::move(temp);
}

This is exactly how std::swap() from the Standard Library is implemented.

Rule of Zero

Earlier in this chapter, the rule of five was introduced. All the discussions so far have been to explain how you have to write those five special member functions: destructor, copy and move constructors, and copy and move assignment operators. However, in modern C++, you should adopt the so-called rule of zero.

The rule of zero states that you should design your classes in such a way that they do not require any of those five special member functions. How do you do that? Basically, you should avoid having any old-style dynamically allocated memory. Instead, use modern constructs such as Standard Library containers. For example, use a vector<vector<SpreadsheetCell>> instead of the SpreadsheetCell** data member in the Spreadsheet class. The vector handles memory automatically, so there is no need for any of those five special member functions.

MORE ABOUT METHODS

C++ also provides myriad choices for methods. This section explains all the tricky details.

static Methods

Methods, like data members, sometimes apply to the class as a whole, not to each object. You can write static methods as well as data members. As an example, consider the SpreadsheetCell class from Chapter 8. It has two helper methods: stringToDouble() and doubleToString(). These methods don’t access information about specific objects, so they could be static. Here is the class definition with these methods static:

class SpreadsheetCell
{
    // Omitted for brevity
    private:
        static std::string doubleToString(double inValue);
        static double stringToDouble(std::string_view inString);
        // Omitted for brevity
};

The implementations of these two methods are identical to the previous implementations. You don’t repeat the static keyword in front of the method definitions. However, note that static methods are not called on a specific object, so they have no this pointer, and are not executing for a specific object with access to its non-static members. In fact, a static method is just like a regular function. The only difference is that it can access private and protected static members of the class. It can also access private and protected non-static data members on objects of the same type, if those objects are made visible to the static method, for example, by passing in a reference or pointer to such an object.

You call a static method just like a regular function from within any method of the class. Thus, the implementation of all the methods in SpreadsheetCell can stay the same. Outside of the class, you need to qualify the method name with the class name using the scope resolution operator. Access control applies as usual.

You might want to make stringToDouble() and doubleToString() public so that other code outside the class can make use of them. If so, you can call them from anywhere like this:

string str = SpreadsheetCell::doubleToString(5.0);

const Methods

A const object is an object whose value cannot be changed. If you have a const, reference to const, or pointer to a const object, the compiler does not let you call any methods on that object unless those methods guarantee that they won’t change any data members. The way you guarantee that a method won’t change data members is to mark the method itself with the const keyword. Here is the SpreadsheetCell class with the methods that don’t change any data members marked as const:

class SpreadsheetCell
{
    public:
        // Omitted for brevity
        double getValue() const;
        std::string getString() const;
        // Omitted for brevity
};

The const specification is part of the method prototype and must accompany its definition as well:

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

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

Marking a method as const signs a contract with client code guaranteeing that you will not change the internal values of the object within the method. If you try to declare a method const that actually modifies a data member, the compiler will complain. You also cannot declare a static method, such as the doubleToString() and stringToDouble() methods from the previous section, const because it is redundant. Static methods do not have an instance of the class, so it would be impossible for them to change internal values. const works by making it appear inside the method that you have a const reference to each data member. Thus, if you try to change the data member, the compiler will flag an error.

You can call const and non-const methods on a non-const object. However, you can only call const methods on a const object. Here are some examples:

SpreadsheetCell myCell(5);
cout << myCell.getValue() << endl;      // OK
myCell.setString("6");                  // OK

const SpreadsheetCell& myCellConstRef = myCell;
cout << myCellConstRef.getValue() << endl; // OK
myCellConstRef.setString("6");             // Compilation Error!

You should get into the habit of declaring const all methods that don’t modify the object so that you can use references to const objects in your program.

Note that const objects can still be destroyed, and their destructor can be called. Nevertheless, destructors are not allowed to be declared const.

mutable Data Members

Sometimes you write a method that is “logically” const but happens to change a data member of the object. This modification has no effect on any user-visible data, but is technically a change, so the compiler won’t let you declare the method const. For example, suppose that you want to profile your spreadsheet application to obtain information about how often data is being read. A crude way to do this would be to add a counter to the SpreadsheetCell class that counts each call to getValue() or getString(). Unfortunately, that makes those methods non-const in the compiler’s eyes, which is not what you intended. The solution is to make your new counter variable mutable, which tells the compiler that it’s okay to change it in a const method. Here is the new SpreadsheetCell class definition:

class SpreadsheetCell
{
    // Omitted for brevity
    private:
        double mValue = 0;
        mutable size_t mNumAccesses = 0;
};

Here are the definitions for getValue() and getString():

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

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

Method Overloading

You’ve already noticed that you can write multiple constructors in a class, all of which have the same name. These constructors differ only in the number and/or types of their parameters. You can do the same thing for any method or function in C++. Specifically, you can overload the function or method name by using it for multiple functions, as long as the number and/or types of the parameters differ. For example, in the SpreadsheetCell class you can rename both setString() and setValue() to set(). The class definition now looks like this:

class SpreadsheetCell
{
    public:
        // Omitted for brevity
        void set(double inValue);
        void set(std::string_view inString);
        // Omitted for brevity
};

The implementations of the set() methods stay the same. When you write code to call set(), the compiler determines which instance to call based on the parameter you pass: if you pass a string_view, the compiler calls the string instance; if you pass a double, the compiler calls the double instance. This is called overload resolution.

You might be tempted to do the same thing for getValue() and getString(): rename each of them to get(). However, that does not work. C++ does not allow you to overload a method name based only on the return type of the method because in many cases it would be impossible for the compiler to determine which instance of the method to call. For example, if the return value of the method is not captured anywhere, the compiler has no way to tell which instance of the method you are trying to call.

Overloading Based on const

You can overload a method based on const. That is, you can write two methods with the same name and same parameters, one of which is declared const and one of which is not. The compiler calls the const method if you have a const object, and the non-const method if you have a non-const object.

Often, the implementation of the const version and the non-const version is identical. To prevent code duplication, you can use the Scott Meyer’s const_cast() pattern. For example, the Spreadsheet class has a method called getCellAt() returning a non-const reference to a SpreadsheetCell. You can add a const overload that returns a const reference to a SpreadsheetCell as follows:

class Spreadsheet
{
    public:
        SpreadsheetCell& getCellAt(size_t x, size_t y);
        const SpreadsheetCell& getCellAt(size_t x, size_t y) const;
        // Code omitted for brevity.
};

Scott Meyer’s const_cast() pattern says that you should implement the const version as you normally would, and implement the non-const version by forwarding the call to the const version with the appropriate casts. Basically, you cast *this to a const Spreadsheet& using std::as_const() (defined in <utility>), call the const version of getCellAt(), and then remove the const from the result by using a const_cast():

const SpreadsheetCell& Spreadsheet::getCellAt(size_t x, size_t y) const
{
    verifyCoordinate(x, y);
    return mCells[x][y];
}

SpreadsheetCell& Spreadsheet::getCellAt(size_t x, size_t y)
{
    return const_cast<SpreadsheetCell&>(std::as_const(*this).getCellAt(x, y));
}

The std::as_const() function is available since C++17. If your compiler doesn’t support it yet, you can use the following static_cast() instead:

return const_cast<SpreadsheetCell&>(
    static_cast<const Spreadsheet&>(*this).getCellAt(x, y));

With these two getCellAt() overloads, you can now call getCellAt() on const and non-const Spreadsheet objects:

Spreadsheet sheet1(5, 6);
SpreadsheetCell& cell1 = sheet1.getCellAt(1, 1);

const Spreadsheet sheet2(5, 6);
const SpreadsheetCell& cell2 = sheet2.getCellAt(1, 1);

In this case, the const version of getCellAt() is not doing much, so you don’t win a lot by using the const_cast() pattern. However, imagine that the const version of getCellAt() is doing more work, then forwarding the non-const to the const version avoids duplicating that code.

Explicitly Deleting Overloads

Overloaded methods can be explicitly deleted, which enables you to disallow calling a method with particular arguments. For example, suppose you have the following class:

class MyClass
{
    public:
        void foo(int i);
};

The foo() method can be called as follows:

MyClass c;
c.foo(123);
c.foo(1.23);

For the third line, the compiler converts the double value (1.23) to an integer value (1) and then calls foo(int i). The compiler might give you a warning, but it will perform this implicit conversion. You can prevent the compiler from performing this conversion by explicitly deleting a double instance of foo():

class MyClass
{
    public:
        void foo(int i);
        void foo(double d) = delete;
};

With this change, an attempt to call foo() with a double will be flagged as an error by the compiler, instead of the compiler performing a conversion to an integer.

Inline Methods

C++ gives you the ability to recommend that a call to a method (or function) should not actually be implemented in the generated code as a call to a separate block of code. Instead, the compiler should insert the method’s body directly into the code where the method is called. This process is called inlining, and methods that want this behavior are called inline methods. Inlining is safer than using #define macros.

You can specify an inline method by placing the inline keyword in front of its name in the method definition. For example, you might want to make the accessor methods of the SpreadsheetCell class inline, in which case you would define them like this:

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

inline std::string SpreadsheetCell::getString() const
{
    mNumAccesses++;
    return doubleToString(mValue);
}

This gives a hint to the compiler to replace calls to getValue() and getString() with the actual method body instead of generating code to make a function call. Note that the inline keyword is just a hint for the compiler. The compiler can ignore it if it thinks it would hurt performance.

There is one caveat: definitions of inline methods (and functions) must be available in every source file in which they are called. That makes sense if you think about it: how can the compiler substitute the method’s body if it can’t see the method definition? Thus, if you write inline methods, you should place the definitions in a header file along with their prototypes.

C++ provides an alternate syntax for declaring inline methods that doesn’t use the inline keyword at all. Instead, you place the method definition directly in the class definition. Here is a SpreadsheetCell class definition with this syntax:

class SpreadsheetCell
{
    public:
        // Omitted for brevity
        double getValue() const { mNumAccesses++; return mValue; }

        std::string getString() const
        {
            mNumAccesses++;
            return doubleToString(mValue);
        }
        // Omitted for brevity
};

Many C++ programmers discover the inline method syntax and employ it without understanding the ramifications of marking a method inline. Marking a method or function as inline only gives a hint to the compiler. Compilers will only inline the simplest methods and functions. If you define an inline method that the compiler doesn’t want to inline, it will silently ignore the hint. Modern compilers will take metrics like code bloat into account before deciding to inline a method or function, and they will not inline anything that is not cost-effective.

Default Arguments

A feature similar to method overloading in C++ is default arguments. You can specify defaults for function and method parameters in the prototype. If the user specifies those arguments, the default values are ignored. If the user omits those arguments, the default values are used. There is a limitation, though: you can only provide defaults for a continuous list of parameters starting from the rightmost parameter. Otherwise, the compiler will not be able to match missing arguments to default arguments. Default arguments can be used in functions, methods, and constructors. For example, you can assign default values to the width and height in your Spreadsheet constructor:

class Spreadsheet
{
    public:
        Spreadsheet(size_t width = 100, size_t height = 100);
        // Omitted for brevity
};

The implementation of the Spreadsheet constructor stays the same. Note that you specify the default arguments only in the method declaration, but not in the definition.

Now you can call the Spreadsheet constructor with zero, one, or two, arguments even though there is only one non-copy constructor:

Spreadsheet s1;
Spreadsheet s2(5);
Spreadsheet s3(5, 6);

A constructor with defaults for all its parameters can function as a default constructor. That is, you can construct an object of that class without specifying any arguments. If you try to declare both a default constructor and a multi-argument constructor with defaults for all its parameters, the compiler will complain because it won’t know which constructor to call if you don’t specify any arguments.

Note that anything you can do with default arguments you can do with method overloading. You could write three different constructors, each of which takes a different number of parameters. However, default arguments allow you to write just one constructor that can take three different number of arguments. You should use the mechanism with which you are most comfortable.

DIFFERENT KINDS OF DATA MEMBERS

C++ gives you many choices for data members. In addition to declaring simple data members in your classes, you can create static data members that all objects of the class share, const members, reference members, const reference members, and more. This section explains the intricacies of these different kinds of data members.

static Data Members

Sometimes giving each object of a class a copy of a variable is overkill or won’t work. The data member might be specific to the class, but not appropriate for each object to have its own copy. For example, you might want to give each spreadsheet a unique numerical identifier. You would need a counter that starts at 0 from which each new object could obtain its ID. This spreadsheet counter really belongs to the Spreadsheet class, but it doesn’t make sense for each Spreadsheet object to have a copy of it because you would have to keep all the counters synchronized somehow. C++ provides a solution with static data members. A static data member is a data member associated with a class instead of an object. You can think of static data members as global variables specific to a class. Here is the Spreadsheet class definition, including the new static counter data member:

class Spreadsheet
{
    // Omitted for brevity
    private:
        static size_t sCounter;
};

In addition to listing static class members in the class definition, you will have to allocate space for them in a source file, usually the source file in which you place your class method definitions. You can initialize them at the same time, but note that unlike normal variables and data members, they are initialized to 0 by default. Static pointers are initialized to nullptr. Here is the code to allocate space for, and zero-initialize, the sCounter member:

size_t Spreadsheet::sCounter;

Static data members are zero-initialized by default, but if you want, you can explicitly initialize them to 0 as follows:

size_t Spreadsheet::sCounter = 0;

This code appears outside of any function or method bodies. It’s almost like declaring a global variable, except that the Spreadsheet:: scope resolution specifies that it’s part of the Spreadsheet class.

image Inline Variables

Starting with C++17, you can declare your static data members as inline. The benefit of this is that you do not have to allocate space for them in a source file. Here’s an example:

class Spreadsheet
{
    // Omitted for brevity
    private:
        static inline size_t sCounter = 0;
};

Note the inline keyword. With this class definition, the following line can be removed from the source file:

size_t Spreadsheet::sCounter;

Accessing static Data Members within Class Methods

You can use static data members as if they were regular data members from within class methods. For example, you might want to create an mId data member for the Spreadsheet class and initialize it from sCounter in the Spreadsheet constructor. Here is the Spreadsheet class definition with an mId member:

class Spreadsheet
{
    public:
        // Omitted for brevity 
        size_t getId() const;
    private:
        // Omitted for brevity
        static size_t sCounter;
        size_t mId = 0;
};

Here is an implementation of the Spreadsheet constructor that assigns the initial ID:

Spreadsheet::Spreadsheet(size_t width, size_t height)
    : mId(sCounter++), mWidth(width), mHeight(height)
{
    mCells = new SpreadsheetCell*[mWidth];
    for (size_t i = 0; i < mWidth; i++) {
        mCells[i] = new SpreadsheetCell[mHeight];
    }
}

As you can see, the constructor can access sCounter as if it were a normal member. The copy constructor should also assign a new ID. This is handled automatically because the Spreadsheet copy constructor delegates to the non-copy constructor, which creates the new ID.

You should not copy the ID in the copy assignment operator. Once an ID is assigned to an object, it should never change. Thus, it’s recommended to make mId a const data member. const data members are discussed later in this chapter.

Accessing static Data Members Outside Methods

Access control specifiers apply to static data members: sCounter is private, so it cannot be accessed from outside class methods. If sCounter was public, you could access it from outside class methods by specifying that the variable is part of the Spreadsheet class with the :: scope resolution operator:

int c = Spreadsheet::sCounter;

However, it’s not recommended to have public data members (const static members discussed in the next section are an exception). You should grant access through public get/set methods. If you want to grant access to a static data member, you need to implement static get/set methods.

const static Data Members

Data members in your class can be declared const, meaning they can’t be changed after they are created and initialized. You should use static const (or const static) data members in place of global constants when the constants apply only to the class, also called class constants. static const data members of integral and enumeration types can be defined and initialized inside the class definition without making them inline variables. For example, you might want to specify a maximum height and width for spreadsheets. If the user tries to construct a spreadsheet with a greater height or width than the maximum, the maximum is used instead. You can make the maximum height and width static const members of the Spreadsheet class:

class Spreadsheet
{
    public:
        // Omitted for brevity
        static const size_t kMaxHeight = 100;
        static const size_t kMaxWidth = 100;
};

You can use these new constants in your constructor as follows:

Spreadsheet::Spreadsheet(size_t width, size_t height)
    : mId(sCounter++)
    , mWidth(std::min(width, kMaxWidth))// std::min() requires <algorithm>
    , mHeight(std::min(height, kMaxHeight))
{
    mCells = new SpreadsheetCell*[mWidth];
    for (size_t i = 0; i < mWidth; i++) {
        mCells[i] = new SpreadsheetCell[mHeight];
    }
}

kMaxHeight and kMaxWidth are public, so you can access them from anywhere in your program as if they were global variables, but with slightly different syntax. You must specify that the variable is part of the Spreadsheet class with the :: scope resolution operator:

cout << "Maximum height is: " << Spreadsheet::kMaxHeight << endl;

These constants can also be used as default values for the constructor parameters. Remember that you can only give default values for a continuous set of parameters starting with the rightmost parameter:

class Spreadsheet
{
    public:
        Spreadsheet(size_t width = kMaxWidth, size_t height = kMaxHeight);
        // Omitted for brevity
};

Reference Data Members

Spreadsheets and SpreadsheetCells are great, but they don’t make a very useful application by themselves. You need code to control the entire spreadsheet program, which you could package into a SpreadsheetApplication class.

The implementation of this class is unimportant at the moment. For now, consider this architecture problem: how can spreadsheets communicate with the application? The application stores a list of spreadsheets, so it can communicate with the spreadsheets. Similarly, each spreadsheet could store a reference to the application object. The Spreadsheet class must then know about the SpreadsheetApplication class, and the SpreadsheetApplication class must know about the Spreadsheet class. This is a circular reference and cannot be solved with normal #includes. The solution is to use a forward declaration in one of the header files. The following is a new Spreadsheet class definition that uses a forward declaration to tell the compiler about the SpreadsheetApplication class. Chapter 11 explains another benefit of forward declarations: they can improve compilation and linking times.

class SpreadsheetApplication; // forward declaration

class Spreadsheet
{
    public:
        Spreadsheet(size_t width, size_t height,
            SpreadsheetApplication& theApp);
        // Code omitted for brevity.
    private:
        // Code omitted for brevity.
        SpreadsheetApplication& mTheApp;
};

This definition adds a SpreadsheetApplication reference as a data member. It’s recommended to use a reference in this case instead of a pointer because a Spreadsheet should always refer to a SpreadsheetApplication. This would not be guaranteed with a pointer.

Note that storing a reference to the application is only done to demonstrate the use of references as data members. It’s not recommended to couple the Spreadsheet and SpreadsheetApplication classes together in this way, but instead to use a paradigm such as MVC (Model-View-Controller), introduced in Chapter 4.

The application reference is given to each Spreadsheet in its constructor. A reference cannot exist without referring to something, so mTheApp must be given a value in the ctor-initializer of the constructor:

Spreadsheet::Spreadsheet(size_t width, size_t height,
    SpreadsheetApplication& theApp)
    : mId(sCounter++)
    , mWidth(std::min(width, kMaxWidth))
    , mHeight(std::min(height, kMaxHeight))
    , mTheApp(theApp)
{
    // Code omitted for brevity.
}

You must also initialize the reference member in the copy constructor. This is handled automatically because the Spreadsheet copy constructor delegates to the non-copy constructor, which initializes the reference member.

Remember that after you have initialized a reference, you cannot change the object to which it refers. It’s not possible to assign to references in the assignment operator. Depending on your use case, this might mean that an assignment operator cannot be provided for your class with reference data members. If that’s the case, the assignment operator is typically marked as deleted.

const Reference Data Members

Your reference members can refer to const objects just as normal references can refer to const objects. For example, you might decide that Spreadsheets should only have a const reference to the application object. You can simply change the class definition to declare mTheApp as a const reference:

class Spreadsheet
{
    public:
        Spreadsheet(size_t width, size_t height,
            const SpreadsheetApplication& theApp);
            // Code omitted for brevity.
    private:
            // Code omitted for brevity.
            const SpreadsheetApplication& mTheApp;
};

There is an important difference between using a const reference versus a non-const reference. The const reference SpreadsheetApplication data member can only be used to call const methods on the SpreadsheetApplication object. If you try to call a non-const method through a const reference, you will get a compilation error.

It’s also possible to have a static reference member or a static const reference member, but you will rarely need something like that.

NESTED CLASSES

Class definitions can contain more than just member functions and data members. You can also write nested classes and structs, declare type aliases, or create enumerated types. Anything declared inside a class is in the scope of that class. If it is public, you can access it outside the class by scoping it with the ClassName:: scope resolution syntax.

You can provide a class definition inside another class definition. For example, you might decide that the SpreadsheetCell class is really part of the Spreadsheet class. And since it becomes part of the Spreadsheet class, you might as well rename it to Cell. You could define both of them like this:

class Spreadsheet
{
    public:
        class Cell
        {
            public:
                Cell() = default;
                Cell(double initialValue);
                // Omitted for brevity
        };

        Spreadsheet(size_t width, size_t height,
            const SpreadsheetApplication& theApp);
        // Remainder of Spreadsheet declarations omitted for brevity
};

Now, the Cell class is defined inside the Spreadsheet class, so anywhere you refer to a Cell outside of the Spreadsheet class, you must qualify the name with the Spreadsheet:: scope. This applies even to the method definitions. For example, the double constructor of Cell now looks like this:

Spreadsheet::Cell::Cell(double initialValue)
    : mValue(initialValue)
{
}

You must even use the syntax for return types (but not parameters) of methods in the Spreadsheet class itself:

Spreadsheet::Cell& Spreadsheet::getCellAt(size_t x, size_t y)
{
    verifyCoordinate(x, y);
    return mCells[x][y];
}

Fully defining the nested Cell class directly inside the Spreadsheet class makes the definition of the Spreadsheet class a bit bloated. You can alleviate this by only including a forward declaration for Cell in the Spreadsheet class, and then defining the Cell class separately, as follows:

class Spreadsheet
{
    public:
        class Cell;

        Spreadsheet(size_t width, size_t height,
            const SpreadsheetApplication& theApp);
        // Remainder of Spreadsheet declarations omitted for brevity
};

class Spreadsheet::Cell
{
    public:
        Cell() = default;
        Cell(double initialValue);
        // Omitted for brevity
};

Normal access control applies to nested class definitions. If you declare a private or protected nested class, you can only use it inside the outer class. A nested class has access to all protected and private members of the outer class. The outer class on the other hand can only access public members of the nested class.

ENUMERATED TYPES INSIDE CLASSES

If you want to define a number of constants inside a class, you should use an enumerated type instead of a collection of #defines. For example, you can add support for cell coloring to the SpreadsheetCell class as follows:

class SpreadsheetCell
{
    public:
        // Omitted for brevity
        enum class Color { Red = 1, Green, Blue, Yellow };
        void setColor(Color color);
        Color getColor() const;
    private:
        // Omitted for brevity
        Color mColor = Color::Red;
};

The implementation of the setColor() and getColor() methods is straightforward:

void SpreadsheetCell::setColor(Color color) { mColor = color; }

SpreadsheetCell::Color SpreadsheetCell::getColor() const { return mColor; }

The new methods can be used as follows:

SpreadsheetCell myCell(5);
myCell.setColor(SpreadsheetCell::Color::Blue);
auto color = myCell.getColor();

OPERATOR OVERLOADING

You often want to perform operations on objects, such as adding them, comparing them, or streaming them to or from files. For example, spreadsheets are really only useful when you can perform arithmetic actions on them, such as summing an entire row of cells.

Example: Implementing Addition for SpreadsheetCells

In true object-oriented fashion, SpreadsheetCell objects should be able to add themselves to other SpreadsheetCell objects. Adding a cell to another cell produces a third cell with the result. It doesn’t change either of the original cells. The meaning of addition for SpreadsheetCells is the addition of the values of the cells.

First Attempt: The add Method

You can declare and define an add() method for your SpreadsheetCell class like this:

class SpreadsheetCell
{
    public:
        // Omitted for brevity
         SpreadsheetCell add(const SpreadsheetCell& cell) const;
        // Omitted for brevity
};

This method adds two cells together, returning a new third cell whose value is the sum of the first two. It is declared const and takes a reference to a const SpreadsheetCell because add() does not change either of the source cells. Here is the implementation:

SpreadsheetCell SpreadsheetCell::add(const SpreadsheetCell& cell) const
{
    return SpreadsheetCell(getValue() + cell.getValue());
}

You can use the add() method like this:

SpreadsheetCell myCell(4), anotherCell(5);
SpreadsheetCell aThirdCell = myCell.add(anotherCell);

That works, but it’s a bit clumsy. You can do better.

Second Attempt: Overloaded operator+ as a Method

It would be convenient to be able to add two cells with the plus sign the way that you add two ints or two doubles—something like this:

SpreadsheetCell myCell(4), anotherCell(5);
SpreadsheetCell aThirdCell = myCell + anotherCell;

C++ allows you to write your own version of the plus sign, called the addition operator, to work correctly with your classes. To do that, you write a method with the name operator+ that looks like this:

class SpreadsheetCell
{
    public:
        // Omitted for brevity
         SpreadsheetCell operator+(const SpreadsheetCell& cell) const;
        // Omitted for brevity
};

The definition of the method is identical to the implementation of the add() method:

SpreadsheetCell SpreadsheetCell::operator+(const SpreadsheetCell& cell) const
{
    return SpreadsheetCell(getValue() + cell.getValue());
}

Now you can add two cells together using the plus sign as shown previously.

This syntax takes a bit of getting used to. Try not to worry too much about the strange method name operator+—it’s just a name like foo or add. In order to understand the rest of the syntax, it helps to understand what’s really going on. When your C++ compiler parses a program and encounters an operator, such as +, -, =, or <<, it tries to find a function or method with the name operator+, operator-, operator=, or operator<<, respectively, that takes the appropriate parameters. For example, when the compiler sees the following line, it tries to find either a method in the SpreadsheetCell class named operator+ that takes another SpreadsheetCell object, or a global function named operator+ that takes two SpreadsheetCell objects:

SpreadsheetCell aThirdCell = myCell + anotherCell;

If the SpreadsheetCell class contains an operator+ method, then the previous line is translated to this:

SpreadsheetCell aThirdCell = myCell.operator+(anotherCell);

Note that there’s no requirement that operator+ takes as a parameter an object of the same type as the class for which it’s written. You could write an operator+ for SpreadsheetCells that takes a Spreadsheet to add to the SpreadsheetCell. That wouldn’t make sense to the programmer, but the compiler would allow it.

Note also that you can give operator+ any return value you want. Operator overloading is a form of function overloading, and recall that function overloading does not look at the return type of the function.

Implicit Conversions

Surprisingly, once you’ve written the operator+ shown earlier, not only can you add two cells together, but you can also add a cell to a string_view, a double, or an int!

SpreadsheetCell myCell(4), aThirdCell;
string str = "hello";
aThirdCell = myCell + string_view(str);
aThirdCell = myCell + 5.6;
aThirdCell = myCell + 4;

The reason this code works is that the compiler does more to try to find an appropriate operator+ than just look for one with the exact types specified. The compiler also tries to find an appropriate conversion for the types so that an operator+ can be found. Constructors that take the type in question are appropriate converters. In the preceding example, when the compiler sees a SpreadsheetCell trying to add itself to double, it finds the SpreadsheetCell constructor that takes a double and constructs a temporary SpreadsheetCell object to pass to operator+. Similarly, when the compiler sees the line trying to add a SpreadsheetCell to a string_view, it calls the string_view SpreadsheetCell constructor to create a temporary SpreadsheetCell to pass to operator+.

This implicit conversion behavior is usually convenient. However, in the preceding example, it doesn’t really make sense to add a string_view to a SpreadsheetCell. You can prevent the implicit construction of a SpreadsheetCell from a string_view by marking that constructor with the explicit keyword:

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

The explicit keyword goes only in the class definition, and only makes sense when applied to constructors that can be called with one argument, such as one-parameter constructors or multi-parameter constructors with default values for parameters.

The selection of an implicit constructor might be inefficient, because temporary objects must be created. To avoid implicit construction for adding a double, you could write a second operator+ as follows:

SpreadsheetCell SpreadsheetCell::operator+(double rhs) const
{
    return SpreadsheetCell(getValue() + rhs);
}

Third Attempt: Global operator+

Implicit conversions allow you to use an operator+ method to add your SpreadsheetCell objects to ints and doubles. However, the operator is not commutative, as shown in the following code:

aThirdCell = myCell + 4;   // Works fine.
aThirdCell = myCell + 5.6; // Works fine.
aThirdCell = 4 + myCell;   // FAILS TO COMPILE!
aThirdCell = 5.6 + myCell; // FAILS TO COMPILE!

The implicit conversion works fine when the SpreadsheetCell object is on the left of the operator, but it doesn’t work when it’s on the right. Addition is supposed to be commutative, so something is wrong here. The problem is that the operator+ method must be called on a SpreadsheetCell object, and that object must be on the left-hand side of the operator+. That’s just the way the C++ language is defined. So, there’s no way you can get this code to work with an operator+ method.

However, you can get it to work if you replace the in-class operator+ method with a global operator+ function that is not tied to any particular object. The function looks like this:

SpreadsheetCell operator+(const SpreadsheetCell& lhs,
    const SpreadsheetCell& rhs)
{
    return SpreadsheetCell(lhs.getValue() + rhs.getValue());    
}

You need to declare the operator in the header file:

class SpreadsheetCell
{
    //Omitted for brevity
};

SpreadsheetCell operator+(const SpreadsheetCell& lhs,
    const SpreadsheetCell& rhs);

Now all four of the addition lines work as you expect:

aThirdCell = myCell + 4;   // Works fine.
aThirdCell = myCell + 5.6; // Works fine.
aThirdCell = 4 + myCell;   // Works fine.
aThirdCell = 5.6 + myCell; // Works fine.

You might be wondering what happens if you write the following code:

aThirdCell = 4.5 + 5.5;

It compiles and runs, but it’s not calling the operator+ you wrote. It does normal double addition of 4.5 and 5.5, which results in the following intermediate statement:

aThirdCell = 10;

To make this assignment work, there should be a SpreadsheetCell object on the right-hand side. The compiler will discover a non-explicit user-defined constructor that takes a double, will use this constructor to implicitly convert the double value into a temporary SpreadsheetCell object, and will then call the assignment operator.

Overloading Arithmetic Operators

Now that you understand how to write operator+, the rest of the basic arithmetic operators are straightforward. Here are the declarations of +, -, *, and /, where you have to replace <op> with +, -, *, and /, resulting in four functions. You can also overload %, but it doesn’t make sense for the double values stored in SpreadsheetCells.

class SpreadsheetCell
{
    // Omitted for brevity
};

SpreadsheetCell operator<op>(const SpreadsheetCell& lhs,
    const SpreadsheetCell& rhs);

The implementations of operator- and operator* are very similar to the implementation of operator+, so these are not shown. For operator/, the only tricky aspect is remembering to check for division by zero. This implementation throws an exception if division by zero is detected:

SpreadsheetCell operator/(const SpreadsheetCell& lhs,
    const SpreadsheetCell& rhs)
{
    if (rhs.getValue() == 0) {
        throw invalid_argument("Divide by zero.");
    }
    return SpreadsheetCell(lhs.getValue() / rhs.getValue());
}

C++ does not require you to actually implement multiplication in operator*, division in operator/, and so on. You could implement multiplication in operator/, division in operator+, and so forth. However, that would be extremely confusing, and there is no good reason to do so except as a practical joke. Whenever possible, stick to the commonly used operator meanings in your implementations.

Overloading the Arithmetic Shorthand Operators

In addition to the basic arithmetic operators, C++ provides shorthand operators such as += and -=. You might assume that writing operator+ for your class also provides operator+=. No such luck. You have to overload the shorthand arithmetic operators explicitly. These operators differ from the basic arithmetic operators in that they change the object on the left-hand side of the operator instead of creating a new object. A second, subtler difference is that, like the assignment operator, they generate a result that is a reference to the modified object.

The arithmetic shorthand operators always require an object on the left-hand side, so you should write them as methods, not as global functions. Here are the declarations for the SpreadsheetCell class:

class SpreadsheetCell
{
    public:
        // Omitted for brevity
        SpreadsheetCell& operator+=(const SpreadsheetCell& rhs);
        SpreadsheetCell& operator-=(const SpreadsheetCell& rhs);
        SpreadsheetCell& operator*=(const SpreadsheetCell& rhs);
        SpreadsheetCell& operator/=(const SpreadsheetCell& rhs);
        // Omitted for brevity
};

Here is the implementation for operator+=. The others are very similar.

SpreadsheetCell& SpreadsheetCell::operator+=(const SpreadsheetCell& rhs)
{
    set(getValue() + rhs.getValue());
    return *this;
}

The shorthand arithmetic operators are combinations of the basic arithmetic and assignment operators. With the previous definitions, you can now write code like this:

SpreadsheetCell myCell(4), aThirdCell(2);
aThirdCell -= myCell;
aThirdCell += 5.4;

You cannot, however, write code like this (which is a good thing!):

5.4 += aThirdCell;

When you have both a normal and a shorthand version of a certain operator, it’s recommended to implement the normal one in terms of the shorthand version to avoid code duplication. For example:

SpreadsheetCell operator+(const SpreadsheetCell& lhs, const SpreadsheetCell& rhs)
{
    auto result(lhs);  // Local copy
    result += rhs;     // Forward to op=() version
    return result;
}

Overloading Comparison Operators

The comparison operators, such as >, <, and ==, are another useful set of operators to define for your classes. Like the basic arithmetic operators, they should be global functions so that you can use implicit conversion on both the left-hand side and the right-hand side of the operator. The comparison operators all return a bool. Of course, you can change the return type, but that’s not recommended.

Here are the declarations, where you have to replace <op> with ==, <, >, !=, <=, and >=, resulting in six functions:

class SpreadsheetCell
{
    // Omitted for brevity
};

bool operator<op>(const SpreadsheetCell& lhs, const SpreadsheetCell& rhs);

Here is the definition of operator==. The others are very similar.

bool operator==(const SpreadsheetCell& lhs, const SpreadsheetCell& rhs)
{
    return (lhs.getValue() == rhs.getValue());
}

In classes with more data members, it might be painful to compare each data member. However, once you’ve implemented == and <, you can write the rest of the comparison operators in terms of those two. For example, here is a definition of operator>= that uses operator<:

bool operator>=(const SpreadsheetCell& lhs, const SpreadsheetCell& rhs)
{
    return !(lhs < rhs);
}

You can use these operators to compare SpreadsheetCells to other SpreadsheetCells, and also to doubles and ints:

if (myCell > aThirdCell || myCell < 10) {
    cout << myCell.getValue() << endl;
}

Building Types with Operator Overloading

Many people find the syntax of operator overloading tricky and confusing, at least at first. The irony is that it’s supposed to make things simpler. As you’ve discovered, that doesn’t mean simpler for the person writing the class, but simpler for the person using the class. The point is to make your new classes as similar as possible to built-in types such as int and double: it’s easier to add objects using + than to remember whether the method name you should call is add() or sum().

At this point, you might be wondering exactly which operators you can overload. The answer is almost all of them—even some you’ve never heard of. You have actually just scratched the surface: you’ve seen the assignment operator in the section on object life cycles, the basic arithmetic operators, the shorthand arithmetic operators, and the comparison operators. Overloading the stream insertion and extraction operators is also useful. In addition, there are some tricky, but interesting, things you can do with operator overloading that you might not anticipate at first. The Standard Library uses operator overloading extensively. Chapter 15 explains how and when to overload the rest of the operators. Chapters 16 to 20 cover the Standard Library.

BUILDING STABLE INTERFACES

Now that you understand all the gory syntax of writing classes in C++, it helps to revisit the design principles from Chapters 5 and 6. Classes are the main unit of abstraction in C++. You should apply the principles of abstraction to your classes to separate the interface from the implementation as much as possible. Specifically, you should make all data members private and provide getter and setter methods for them. This is how the SpreadsheetCell class is implemented. mValue is private; set() sets this value, while getValue() and getString() retrieve the value.

Using Interface and Implementation Classes

Even with the preceding measures and the best design principles, the C++ language is fundamentally unfriendly to the principle of abstraction. The syntax requires you to combine your public interfaces and private (or protected) data members and methods together in one class definition, thereby exposing some of the internal implementation details of the class to its clients. The downside of this is that if you have to add new non-public methods or data members to your class, all the clients of the class have to be recompiled. This can become a burden in bigger projects.

The good news is that you can make your interfaces a lot cleaner and hide all implementation details, resulting in stable interfaces. The bad news is that it takes a bit of coding. The basic principle is to define two classes for every class you want to write: the interface class and the implementation class. The implementation class is identical to the class you would have written if you were not taking this approach. The interface class presents public methods identical to those of the implementation class, but it only has one data member: a pointer to an implementation class object. This is called the pimpl idiom, private implementation idiom, or bridge pattern. The interface class method implementations simply call the equivalent methods on the implementation class object. The result of this is that no matter how the implementation changes, it has no impact on the public interface class. This reduces the need for recompilation. None of the clients that use the interface class need to be recompiled if the implementation (and only the implementation) changes. Note that this idiom only works if the single data member is a pointer to the implementation class. If it were a by-value data member, the clients would have to be recompiled when the definition of the implementation class changes.

To use this approach with the Spreadsheet class, define a public interface class, Spreadsheet, that looks like this:

#include "SpreadsheetCell.h"
#include <memory>

// Forward declarations
class SpreadsheetApplication;

class Spreadsheet
{
    public:
        Spreadsheet(const SpreadsheetApplication& theApp,
            size_t width = kMaxWidth, size_t height = kMaxHeight);
        Spreadsheet(const Spreadsheet& src);
        ~Spreadsheet();

        Spreadsheet& operator=(const Spreadsheet& rhs);

        void setCellAt(size_t x, size_t y, const SpreadsheetCell& cell);
        SpreadsheetCell& getCellAt(size_t x, size_t y);

        size_t getId() const;

        static const size_t kMaxHeight = 100;
        static const size_t kMaxWidth = 100;

        friend void swap(Spreadsheet& first, Spreadsheet& second) noexcept;

    private:
        class Impl;
        std::unique_ptr<Impl> mImpl;
};

The implementation class, Impl, is a private nested class, because no one else besides the Spreadsheet class needs to know about the implementation class. The Spreadsheet class now contains only one data member: a pointer to an Impl instance. The public methods are identical to the old Spreadsheet.

The nested Spreadsheet::Impl class has almost the same interface as the original Spreadsheet class. However, because the Impl class is a private nested class of Spreadsheet, you cannot have the following global friend swap() function that swaps two Spreadsheet::Impl objects:

friend void swap(Spreadsheet::Impl& first, Spreadsheet::Impl& second) noexcept;

Instead, a private swap() method is defined for the Spreadsheet::Impl class as follows:

void swap(Impl& other) noexcept;

The implementation is straightforward, but you need to remember that this is a nested class, so you need to specify Spreadsheet::Impl::swap() instead of just Impl::swap(). The same holds true for the other members. For details, see the section on nested classes earlier in this chapter. Here is the swap() method:

void Spreadsheet::Impl::swap(Impl& other) noexcept
{
    using std::swap;

    swap(mWidth, other.mWidth);
    swap(mHeight, other.mHeight);
    swap(mCells, other.mCells);
}

Now that the Spreadsheet class has a unique_ptr to the implementation class, the Spreadsheet class needs to have a user-declared destructor. Since we don’t need to do anything in this destructor, it can be defaulted in the implementation file as follows:

Spreadsheet::~Spreadsheet() = default;

This shows that you can default a special member function not only in the class definition, but also in the implementation file.

The implementations of the Spreadsheet methods, such as setCellAt() and getCellAt(), just pass the request on to the underlying Impl object:

void Spreadsheet::setCellAt(size_t x, size_t y, const SpreadsheetCell& cell)
{
    mImpl->setCellAt(x, y, cell);
}

SpreadsheetCell& Spreadsheet::getCellAt(size_t x, size_t y)
{
    return mImpl->getCellAt(x, y);
}

The constructors for the Spreadsheet must construct a new Impl to do its work:

Spreadsheet::Spreadsheet(const SpreadsheetApplication& theApp,
    size_t width, size_t height) 
{
    mImpl = std::make_unique<Impl>(theApp, width, height);
}

Spreadsheet::Spreadsheet(const Spreadsheet& src)
{
    mImpl = std::make_unique<Impl>(*src.mImpl);
}

The copy constructor looks a bit strange because it needs to copy the underlying Impl from the source spreadsheet. The copy constructor takes a reference to an Impl, not a pointer, so you must dereference the mImpl pointer to get to the object itself so the constructor call can take its reference.

The Spreadsheet assignment operator must similarly pass on the assignment to the underlying Impl:

Spreadsheet& Spreadsheet::operator=(const Spreadsheet& rhs)
{
    *mImpl = *rhs.mImpl;
    return *this;
}

The first line in the assignment operator looks a little odd. The Spreadsheet assignment operator needs to forward the call to the Impl assignment operator, which only runs when you copy direct objects. By dereferencing the mImpl pointers, you force direct object assignment, which causes the assignment operator of Impl to be called.

The swap() function simply swaps the single data member:

void swap(Spreadsheet& first, Spreadsheet& second) noexcept
{
    using std::swap;

    swap(first.mImpl, second.mImpl);
} 

This technique to truly separate interface from implementation is powerful. Although it is a bit clumsy at first, once you get used to it, you will find it natural to work with. However, it’s not common practice in most workplace environments, so you might find some resistance to trying it from your coworkers. The most compelling argument in favor of it is not the aesthetic one of splitting out the interface, but the speedup in build time if the implementation of the class changes. When a class is not using the pimpl idiom, a change to its implementation details might trigger a long build. For example, adding a new data member to a class definition triggers a rebuild of all other source files that include this class definition. With the pimpl idiom, you can modify the implementation class definition as much as you like, as long as the public interface class remains untouched, it won’t trigger a long build.

An alternative to separating the implementation from the interface is to use an abstract interface—that is, an interface with only pure virtual methods—and then have an implementation class that implements that interface. See Chapter 10 for a discussion on abstract interfaces.

SUMMARY

This chapter, along with Chapter 8, provided all the tools you need to write solid, well-designed classes, and to use objects effectively.

You discovered that dynamic memory allocation in objects presents new challenges: you need to implement a destructor, copy constructor, copy assignment operator, move constructor, and move assignment operator, which properly copy, move, and free your memory. You learned how to prevent assignment and pass-by-value by explicitly deleting the copy constructor and assignment operator. You discovered the copy-and-swap idiom to implement copy assignment operators, and learned about the rule of zero.

You read more about different kinds of data members, including static, const, const reference, and mutable members. You also learned about static, inline, and const methods, method overloading, and default arguments. This chapter also described nested class definitions, and friend classes, functions, and methods.

You encountered operator overloading, and learned how to overload the arithmetic and comparison operators, both as global friend functions and as class methods.

Finally, you learned how to take abstraction to the extreme by providing separate interface and implementation classes.

Now that you’re fluent in the language of object-oriented programming, it’s time to tackle inheritance, which is covered next in Chapter 10.

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

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