Chapter 7

Mastering Classes and Objects

WHAT’S IN THIS CHAPTER?

  • How to use dynamic memory allocation in objects
  • The different kinds of data members you can have (static, const, reference)
  • The different kinds of methods you can implement (static, const, inline)
  • The details of method overloading
  • How to work with default parameters
  • How to use nested classes
  • How to make classes friends of other classes
  • What operator overloading is
  • How to write separate interface and implementation classes.

Chapter 6 started the discussion on classes and objects. Now it’s time to master their subtleties so you can use them to their full potential. By reading this chapter, you will learn how to manipulate and exploit some of the most complicated aspects of the C++ language in order to write safe, effective, and useful classes.

Many of the concepts in this chapter arise in advanced C++ programming, especially in the standard template library.

DYNAMIC MEMORY ALLOCATION IN OBJECTS

Sometimes you don’t know how much memory you will need before your program actually runs. As you know, 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 6 introduced the SpreadsheetCell class. This chapter moves on to write the Spreadsheet class. As with the SpreadsheetCell class, the Spreadsheet class will evolve 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:

image
#include "SpreadsheetCell.h"
class Spreadsheet
{
    public:
        Spreadsheet(int inWidth, int inHeight);
        void setCellAt(int x, int y, const SpreadsheetCell& cell);
        SpreadsheetCell getCellAt(int x, int y);
    protected:
        bool inRange(int val, int upper);
        int mWidth, mHeight;
        SpreadsheetCell** mCells;
};

Code snippet from SpreadsheetSpreadsheet.h

pen.gif

The Spreadsheet class uses normal pointers for the mCells array. This is done throughout this chapter to show the consequences and to explain how you should handle dynamic memory in classes. In production code, you should use one of the standard C++ containers, like std::vector which is briefly introduced in Chapter 1 and discussed in detail in Chapter 12.

Note that the Spreadsheet class does not contain a standard two-dimensional array of SpreadsheetCells. Instead, it contains a SpreadsheetCell**. The reason is that 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:

image
#include "Spreadsheet.h"
Spreadsheet::Spreadsheet(int inWidth, int inHeight) :
    mWidth(inWidth), mHeight(inHeight)
{
    mCells = new SpreadsheetCell* [mWidth];
    for (int i = 0; i < mWidth; i++) {
        mCells[i] = new SpreadsheetCell[mHeight];
    }
}

Code snippet from SpreadsheetSpreadsheet.cpp

The resultant memory for a Spreadsheet called s1 on the stack with width four and height three is shown in Figure 7-1.

The implementations of the set and retrieval methods are straightforward:

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

Code snippet from SpreadsheetSpreadsheet.cpp

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 10.

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 will be called when the object is destroyed. Here is the Spreadsheet class definition from earlier with a destructor:

image
class Spreadsheet
{
    public:
        Spreadsheet(int inWidth, int inHeight);
        ~Spreadsheet();
        // Code omitted for brevity
};

Code snippet from SpreadsheetSpreadsheet.h

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.

Here is the implementation of the Spreadsheet class destructor:

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

Code snippet from SpreadsheetSpreadsheet.cpp

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 6 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.

image
#include "Spreadsheet.h"
void printSpreadsheet(Spreadsheet s)
{
   // Code omitted for brevity.
}
int main()
{
    Spreadsheet s1(4, 3);
    printSpreadsheet(s1);
    return 0;
}

Code snippet from SpreadsheetSpreadsheetTest.cpp

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 7-2.

If s were to change something to which mCells points, that change would show 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 7-3.

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

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

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

Code snippet from SpreadsheetSpreadsheetTest.cpp

After both objects are constructed, you would have the memory layout shown in Figure 7-4.

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

Now, not only do the mCells pointers in s1 and s2 point to the same memory, but also you have orphaned the memory to which mCells in s1 previously pointed. That is why in assignment operators you must first free the memory referenced by the left-hand side, and then do a deep copy.

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

cross.gif

Whenever you have dynamically allocated memory in a class, you should write your own copy constructor and assignment operator to provide a deep copy of the memory.

The Spreadsheet Copy Constructor

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

image
class Spreadsheet
{
    public:
        Spreadsheet(int inWidth, int inHeight);
        Spreadsheet(const Spreadsheet& src);
        // Code omitted for brevity
};

Code snippet from SpreadsheetSpreadsheet.h

Here is a first definition of the copy constructor:

image
Spreadsheet::Spreadsheet(const Spreadsheet& src)
{
    mWidth = src.mWidth;
    mHeight = src.mHeight;
    mCells = new SpreadsheetCell* [mWidth];
    for (int i = 0; i < mWidth; i++) {
        mCells[i] = new SpreadsheetCell[mHeight];
    }
    for (int i = 0; i < mWidth; i++) {
        for (int j = 0; j < mHeight; j++) {
            mCells[i][j] = src.mCells[i][j];
        }
    }
}

Code snippet from SpreadsheetSpreadsheetNoCopyFrom.cpp

Note that the copy constructor copies all data members, including mWidth and mHeight, not just the pointer data members. The rest of the code in the copy constructor provides 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 no existing mCells yet in this object.

The Spreadsheet Assignment Operator

Here is the definition for the Spreadsheet class with an assignment operator:

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

Code snippet from SpreadsheetSpreadsheet.h

Here is the implementation of the assignment operator for the Spreadsheet class, with explanations interspersed. Note that when an object is assigned to, it already has been initialized. Thus, you must free any dynamically allocated memory before allocating new memory. You can think of an assignment operator as a combination of a destructor and a copy constructor. You are essentially “reincarnating” the object with new life (or data) when you assign to it.

The first lines of code in any assignment operator checks for self-assignment.

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

Code snippet from SpreadsheetSpreadsheetNoCopyFrom.cpp

This self-assignment check is required, not only for efficiency, but also for correctness. If the preceding self-assignment test was removed, the code will most likely crash on self-assignment, because the second step in the code deletes mCells for the left-hand side and afterwards copies mCells from the right-hand side to the left-hand side. In the case of self-assignment, both sides are the same, so during copying you would access dangling pointers.

Because this is an assignment operator, the object being assigned to already has mCells initialized. You need to free these cells up.

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

Code snippet from SpreadsheetSpreadsheetNoCopyFrom.cpp

This chunk of code is identical to the destructor. You must free all the memory before reallocating it, or you will create a memory leak. The next step is to copy the memory.

image
    // Copy the new memory.
    mWidth = rhs.mWidth;
    mHeight = rhs.mHeight;
    mCells = new SpreadsheetCell* [mWidth];
    for (int i = 0; i < mWidth; i++) {
        mCells[i] = new SpreadsheetCell[mHeight];
    }
    for (int i = 0; i < mWidth; i++) {
        for (int j = 0; j < mHeight; j++) {
            mCells[i][j] = rhs.mCells[i][j];
        }
    }
    return *this;
}

Code snippet from SpreadsheetSpreadsheetNoCopyFrom.cpp

Note that this code looks remarkably like the code in the copy constructor. The following section will explain how you can avoid this code duplication.

The assignment operator completes the “big 3” routines for managing dynamically allocated memory in an object: the destructor, the copy constructor, and the assignment operator. Whenever you find yourself writing one of those methods you should write all of them.

cross.gif

Whenever a class dynamically allocates memory, write a destructor, copy constructor, and assignment operator.

pen.gif

C++11 includes a new concept called move semantics, which requires a move constructor and move assignment operator. These can be used to increase performance in certain situations and are discussed in detail in Chapter 9.

Common Helper Routines for Copy Constructor and Assignment Operator

The copy constructor and the assignment operator are quite similar. Thus, it’s usually convenient to factor the common tasks into a helper method. For example, you could add a copyFrom() method to the Spreadsheet class, and rewrite the copy constructor and assignment operator to use it like this:

image
void Spreadsheet::copyFrom(const Spreadsheet& src)
{
    mWidth = src.mWidth;
    mHeight = src.mHeight;
    mCells = new SpreadsheetCell* [mWidth];
    for (int i = 0; i < mWidth; i++) {
        mCells[i] = new SpreadsheetCell[mHeight];
    }
    for (int i = 0; i < mWidth; i++) {
        for (int j = 0; j < mHeight; j++) {
            mCells[i][j] = src.mCells[i][j];
        }
    }
}
Spreadsheet::Spreadsheet(const Spreadsheet& src)
{
    copyFrom(src);
}
Spreadsheet& Spreadsheet::operator=(const Spreadsheet& rhs)
{
    // Check for self-assignment.
    if (this == &rhs) {
        return *this;
    }
    // Free the old memory.
    for (int i = 0; i < mWidth; i++) {
        delete [] mCells[i];
    }
    delete [] mCells;
    mCells = nullptr;
    // Copy the new memory.
    copyFrom(rhs);
    return *this;
}

Code snippet from SpreadsheetSpreadsheet.cpp

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 marking your operator= and copy constructor private. 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:

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

Code snippet from SpreadsheetNoCopyAssignSpreadsheet.h

You don’t need to provide implementations for private copy constructors and assignment operators. 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:

 '=' : cannot access private member declared in class 'Spreadsheet'. 
pen.gif

Instead of making your operator= and copy constructor private, you can make them protected if you want to allow subclasses to use them. Subclasses are discussed in Chapter 8.

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:

image
class Spreadsheet
{
    // Omitted for brevity
    protected:
        static int sCounter = 0;
};

Code snippet from SpreadsheetDataMembersSpreadsheet.h

With C++11, that’s all you need to do. If you are using C++ prior to C++11, it is a bit clumsier. In that case, you cannot do initialization in the class definition. 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 initialize the sCounter member in pre-C++11:

image
int Spreadsheet::sCounter = 0;

Code snippet from SpreadsheetDataMembersSpreadsheet.cpp

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.

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 member of the Spreadsheet class and initialize it from the sCounter member in the Spreadsheet constructor. Here is the Spreadsheet class definition with an mId member:

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

Code snippet from SpreadsheetDataMembersSpreadsheet.h

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

image
Spreadsheet::Spreadsheet(int inWidth, int inHeight) :
    mWidth(inWidth), mHeight(inHeight)
{
    mId = sCounter++;
    mCells = new SpreadsheetCell* [mWidth];
    for (int i = 0; i < mWidth; i++) {
        mCells[i] = new SpreadsheetCell[mHeight];
    }
}

Code snippet from SpreadsheetDataMembersSpreadsheet.cpp

As you can see, the constructor can access sCounter as if it were a normal member. Remember to assign an ID in the copy constructor as well:

image
Spreadsheet::Spreadsheet(const Spreadsheet& src)
{
    mId = sCounter++;
    copyFrom(src);
}

Code snippet from SpreadsheetDataMembersSpreadsheet.cpp

You should not copy the ID in the assignment operator. Once an ID is assigned to an object it should never change.

Accessing static Data Members Outside Methods

Access control specifiers apply to static data members: sCounter is protected, 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. 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, which are explained later in this chapter.

const Data Members

Data members in your class can be declared const, meaning they can’t be changed after they are created and initialized. Constants almost never make sense at the object level, so const data members are usually static as well. You should use static const data members in place of global constants when the constants apply only to the class. 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 max height and width static const members of the Spreadsheet class:

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

Code snippet from SpreadsheetDataMembersSpreadsheet.h

You can use these new constants in your constructor as shown in the following section of code (note the use of the ternary operator):

image
Spreadsheet::Spreadsheet(int inWidth, int inHeight) :
    mWidth(inWidth < kMaxWidth ? inWidth : kMaxWidth),
    mHeight(inHeight < kMaxHeight ? inHeight : kMaxHeight)
{
    mId = sCounter++;
    mCells = new SpreadsheetCell* [mWidth];
    for (int i = 0; i < mWidth; i++) {
        mCells[i] = new SpreadsheetCell[mHeight];
    }
}

Code snippet from SpreadsheetDataMembersSpreadsheet.cpp

pen.gif

Instead of automatically clamping the width and height to their maximum, you could also decide to throw an exception when the width or height exceed their maximum. However, the destructor will not be called when you throw an exception from a constructor. So, you need to be careful with this. Details are explained in Chapter 10.

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;

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 whole 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 should store a reference to the application object. The Spreadsheet class must 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 (see Chapter 9 for details). Here is the new Spreadsheet class definition that uses a forward declaration to tell the compiler about the SpreadsheetApplication class:

image
class SpreadsheetApplication; // forward declaration
class Spreadsheet
{
    public:
        Spreadsheet(int inWidth, int inHeight,
            SpreadsheetApplication& theApp);
        // Code omitted for brevity.
    protected:
        // Code omitted for brevity.
        SpreadsheetApplication& mTheApp;
};

Code snippet from SpreadsheetDataMembersSpreadsheet.h

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 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:

image
Spreadsheet::Spreadsheet(int inWidth, int inHeight,
    SpreadsheetApplication& theApp)
    : mWidth(inWidth < kMaxWidth ? inWidth : kMaxWidth),
    mHeight(inHeight < kMaxHeight ? inHeight : kMaxHeight), mTheApp(theApp)
{
    // Code omitted for brevity.
}

Code snippet from SpreadsheetDataMembersSpreadsheet.cpp

You must also initialize the reference member in the copy constructor:

image
Spreadsheet::Spreadsheet(const Spreadsheet& src) :
    mTheApp(src.mTheApp)
{
    mId = sCounter++;
    copyFrom(src);
}

Code snippet from SpreadsheetDataMembersSpreadsheet.cpp

Remember that after you have initialized a reference you cannot change the object to which it refers. Thus, you do not need to attempt to assign to references in the assignment operator.

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:

image
class Spreadsheet
{
    public:
        Spreadsheet(int inWidth, int inHeight,
            const SpreadsheetApplication& theApp);
        // Code omitted for brevity.
protected:
        // Code omitted for brevity.
        const SpreadsheetApplication& mTheApp;
};

Code snippet from SpreadsheetDataMembersSpreadsheet.h

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 compiler error.

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

MORE ABOUT METHODS

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

static Methods

Methods, like members, sometimes apply to the class as a whole, not to each object. You can write static methods as well as members. As an example, consider the SpreadsheetCell class from Chapter 6. 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:

image
class SpreadsheetCell
{
    // Omitted for brevity
    protected:
        static string doubleToString(double val);
        static double stringToDouble(const string& str);
        // Omitted for brevity
};

Code snippet from SpreadsheetCellMethodsSpreadsheetCell.h

These methods are not declared as const anymore because it’s not allowed to declare static methods as const. The non-static versions of those methods were marked as const.

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 data members of the class. It can also access private and protected non-static data members on other objects of the same type, if those other objects are made visible to the static method, for example by passing in a reference or pointer to such object.

You call a static method just like a regular function from within any method of the class. Thus, the implementation of all 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 (as for static members). Access control applies as usual.

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

string str = SpreadsheetCell::doubleToString(5);

const Methods

A const object is an object whose value cannot be changed. If you have a const, reference to const or pointer to const object, the compiler will 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 member marked const:

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

Code snippet from SpreadsheetCellMethodsSpreadsheetCell.h

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

image
double SpreadsheetCell::getValue() const
{
    return mValue;
}
string SpreadsheetCell::getString() const
{
    return mString;
}

Code snippet from SpreadsheetCellMethodsSpreadsheetCell.h

Marking a method as const signs a contract with client code guaranteeing that you will not try to 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 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.

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

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

Code snippet from SpreadsheetCellMethodsSpreadsheetCellTest.cpp

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. You shouldn’t try to mark the destructor 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 info 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:

image
class SpreadsheetCell
{
    // Omitted for brevity
    protected:
        double mValue;
        string mString;
        mutable int mNumAccesses = 0;
};

Code snippet from SpreadsheetCellMethodsSpreadsheetCell.h

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

image
double SpreadsheetCell::getValue() const
{
    mNumAccesses++;
    return mValue;
}
string SpreadsheetCell::getString() const
{
    mNumAccesses++;
    return mString;
}

Code snippet from SpreadsheetCellMethodsSpreadsheetCell.h

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 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 or types of the parameters differ. For example, in the SpreadsheetCell class you could rename both setString() and setValue() to set(). The class definition now looks like this:

image
class SpreadsheetCell
{
    public:
        // Omitted for brevity
        void set(double inValue);
        void set(const string& inString);
        // Omitted for brevity
};

Code snippet from SpreadsheetCellMethodsSpreadsheetCell.h

The implementations of the set() methods stay the same. Note that the double constructor that previously called setValue() must now call set(). 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 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 compile. 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 wanted.

Note also that 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 will call the const method if you have a const object and the non-const method if you have a non-const object.

imageOverloaded methods can be explicitly deleted, which can be used to disallow calling a member function with particular parameters. 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 second line, the compiler will convert the double value (1.23) to an integer value (1) and then call 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 performing a conversion to an integer.

Default Parameters

A feature similar to method overloading in C++ is default parameters. You can specify defaults for function and method parameters in the prototype. If the user specifies those arguments, the defaults 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 would not be able to match missing arguments to default parameters. Default parameters are most useful in constructors. For example, you can assign default values to the width and height in your Spreadsheet constructor:

image
class Spreadsheet
{
    public:
        Spreadsheet(const SpreadsheetApplication& theApp,
            int inWidth = kMaxWidth, int inHeight = kMaxHeight);
        // Omitted for brevity
};

Code snippet from SpreadsheetDefaultParamsSpreadsheet.h

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

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

image
SpreadsheetApplication theApp;
 Spreadsheet s1(theApp);
 Spreadsheet s2(theApp, 5);
 Spreadsheet s3(theApp, 5, 6);

Code snippet from SpreadsheetDefaultParamsSpreadsheetTest.cpp

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 parameters you can do with method overloading. You could write three different constructors, each of which takes a different number of parameters. However, default parameters allow you to write only one constructor to take three different numbers of arguments. You should use the mechanism with which you are most comfortable.

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 or function body directly into the code where the method or function call is made. This process is called inlining, and methods or functions that want this behavior are called inline methods or functions. Inlining is safer than using #define macros.

You can specify an inline method or function by placing the inline keyword in front of its name in the function or 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:

image
inline double SpreadsheetCell::getValue() const
{
    mNumAccesses++;
    return mValue;
}
inline string SpreadsheetCell::getString() const
{
    mNumAccesses++;
    return mString;
}

Code snippet from SpreadsheetCellMethodsSpreadsheetCell.h

Now, the compiler has the option to replace calls to getValue() and getString() with the actual method body instead of generating code to make a function call.

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 function body if it can’t see the function definition? Thus, if you write inline functions or methods you should place the definitions in a header file along with their prototypes. For methods, this means placing the definitions in the .h file that includes the class definition. This placement is perfectly safe: The linker doesn’t complain about multiple definitions of the same method.

pen.gif

Advanced C++ compilers do not require you to put definitions of inline methods in a header file. For example, Microsoft Visual C++ supports Link-Time Code Generation (LTCG) which will automatically inline small function bodies, even if they are not declared as inline and even if they are not defined in a header file. When you use such a compiler, make use of it, and don’t put the definitions in the header file. This way, your interface files stay clean without any implementation details visible in it.

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:

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

Code snippet from SpreadsheetCellMethodsSpreadsheetCell.h

pen.gif

If you single-step with a debugger on a function call that is inlined, some advanced C++ debuggers will jump to the actual source code of the inline function in the header file, giving you the illusion of a function call, but in reality, the code is inlined.

Many C++ programmers discover the inline method syntax and employ it without understanding the ramifications of making a method inline. First, there are many restrictions on which methods can be inline. Compilers will only inline the simplest methods and functions. If you define an inline method that the compiler doesn’t want to inline, it may silently ignore the directive. Second, big inline methods can lead to code bloat. The body of the methods are reproduced everywhere they are called, increasing the size of your program executable.

Modern compilers like Microsoft Visual C++ will take metrics like code bloat into account before deciding to inline a method, and they will not inline anything that is not cost-effective.

NESTED CLASSES

Class definitions can contain more than just methods and members. You can also write nested classes and structs, declare typedefs, 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. You could define both of them like this:

image
class Spreadsheet
{
    public:
        class SpreadsheetCell
        {
            public:
                SpreadsheetCell();
                SpreadsheetCell(double initialValue);
                // Omitted for brevity
            protected:
                double mValue;
                string mString;
                mutable int mNumAccesses;
        };
        Spreadsheet(const SpreadsheetApplication& theApp,
            int inWidth = kMaxWidth, int inHeight = kMaxHeight);
        Spreadsheet(const Spreadsheet& src);
        ~Spreadsheet();
        // Remainder of Spreadsheet declarations omitted for brevity
};

Code snippet from NestedClassesSpreadsheet.h

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

image
Spreadsheet::SpreadsheetCell::SpreadsheetCell() : mValue(0), mNumAccesses(0)
{
}

Code snippet from NestedClassesSpreadsheet.cpp

This syntax can quickly become clumsy. For example, the definition of the SpreadsheetCell assignment operator now looks like this:

image
Spreadsheet::SpreadsheetCell& Spreadsheet::SpreadsheetCell::operator=(
    const SpreadsheetCell& rhs)
{
    if (this == &rhs) {
        return *this;
    }
    mValue = rhs.mValue;
    mString = rhs.mString;
    mNumAccesses = rhs.mNumAccesses;
    return *this;
}

Code snippet from NestedClassesSpreadsheet.cpp

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

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

Code snippet from NestedClassesSpreadsheet.cpp

You can avoid the clumsy syntax by using a typedef to rename Spreadsheet::SpreadsheetCell to something more manageable like SCell:

image
typedef Spreadsheet::SpreadsheetCell SCell;

Code snippet from NestedClassesSpreadsheet.h

This typedef should go outside the Spreadsheet class definition, or else you will have to qualify the typedef name itself with Spreadsheet:: to get Spreadsheet::SCell. That wouldn’t do you much good!

Now you can write your constructor like this:

image
SCell::SpreadsheetCell() : mValue(0), mNumAccesses(0)  
{
}

Code snippet from NestedClassesSpreadsheet.cpp

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.

You should generally use nested class definitions only for trivial classes. It is really too clumsy for something like the SpreadsheetCell 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:

image
class SpreadsheetCell
{
    public:
        // Omitted for brevity
        typedef enum {Red=1, Green, Blue, Yellow} Colors;
        void setColor(Colors color);
    protected:
        // Omitted for brevity
        Colors mColor = Red;
};

Code snippet from SpreadsheetCellColorsSpreadsheetCell.h

The implementation of the setColor() method is straightforward:

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

Code snippet from SpreadsheetCellColorsSpreadsheetCell.cpp

The new method can be used as follows:

image
SpreadsheetCell myCell(5);
myCell.setColor(SpreadsheetCell::Blue);

Code snippet from SpreadsheetCellColorsSpreadsheetCellTest.cpp

Using an enumerated type is the preferred solution instead of using #defines as follows:

#define SPREADSHEETCELL_RED 1
#define SPREADSHEETCELL_GREEN 2
#define SPREADSHEETCELL_BLUE 3
#define SPREADSHEETCELL_YELLOW 4
class SpreadsheetCell
{
    public:
        // Omitted for brevity
        void setColor(int color);
    protected:
        // Omitted for brevity
        int mColor;
};

When you use #defines, you have to use an integer parameter for the setColor() function instead of a clear type like the Colors enumerated type.

FRIENDS

C++ allows classes to declare that other classes or member functions of other classes or nonmember functions are friends, and can access protected and private data members and methods. For example, the SpreadsheetCell class could specify that the Spreadsheet class is its “friend” like this:

class SpreadsheetCell
{
    public:
        friend class Spreadsheet;
        // Remainder of the class omitted for brevity
};

Now all the methods of the Spreadsheet class can access the private and protected data members and methods of the SpreadsheetCell class.

If you only want to make a specific member function of the Spreadsheet class a friend, you can do that as follows:

image
class SpreadsheetCell
{
    public:
        friend void Spreadsheet::setCellAt(int x, int y,
            const SpreadsheetCell& cell);
        // Remainder of the class omitted for brevity
};

Code snippet from FriendsFriendMethodSpreadsheetCell.h

Note that a class needs to know which other classes, methods, or functions wish 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.

You might, for example, want to write a function to verify that the string of a SpreadsheetCell object is not empty. You might want this verification routine to be outside the SpreadsheetCell 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 SpreadsheetCell class definition with a friend checkSpreadsheetCell() function:

image
class SpreadsheetCell
{
    public:
        friend bool checkSpreadsheetCell(const SpreadsheetCell& cell);
        // Omitted for brevity
};

Code snippet from FriendsSpreadsheetCell.h

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:

image
bool checkSpreadsheetCell(const SpreadsheetCell& cell)
{
    return !(cell.mString.empty());
}

Code snippet from FriendsSpreadsheetCell.cpp

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

friend classes and methods are easy to abuse; they allow you to violate the principle of abstraction by exposing internals of your class to other classes or functions. Thus, you should use them only in limited circumstances such as operator overloading because in that case you need access to protected and private members, as discussed in the next section.

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. The string representations are ignored.

First Attempt: The add Method

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

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

Code snippet from OperatorOverloadingAddFirstAttemptSpreadsheetCell.h

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. It returns a const SpreadsheetCell because you don’t want users to change the return value. They should just assign it to another object. add() is a method, so it is called on one object and passed another. Here is the implementation:

image
const SpreadsheetCell SpreadsheetCell::add(const SpreadsheetCell& cell) const
{
    SpreadsheetCell newCell;
    newCell.set(mValue + cell.mValue); // update mValue and mString
    return newCell;
}

Code snippet from OperatorOverloadingAddFirstAttemptSpreadsheetCell.cpp

Note that the implementation creates a new SpreadsheetCell called newCell and returns a copy of that cell. You might be tempted to return a reference to the cell instead. However, that will not work because as soon as the add() method ends and newCell goes out of scope it will be destroyed. The reference that you returned will then be a dangling reference.

You can use the add() method like this:

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

Code snippet from OperatorOverloadingAddFirstAttemptSpreadsheetCellTest.cpp

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:

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

Code snippet from OperatorOverloadingAddSecondAttemptSpreadsheetCellTest.cpp

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:

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

Code snippet from OperatorOverloadingAddSecondAttemptSpreadsheetCell.h

pen.gif

You are allowed to write spaces between operator and the plus sign. For example, instead of writing operator+, you can write operator +. This is true for all operators. This book adopts the style without spaces.

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

image
const SpreadsheetCell
SpreadsheetCell::operator+(const SpreadsheetCell& cell) const
{
    SpreadsheetCell newCell;
    newCell.set(mValue + cell.mValue); // update mValue and mString.
    return newCell;
}

Code snippet from OperatorOverloadingAddSecondAttemptSpreadsheetCell.cpp

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;

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, you can also add a cell to a string, a double, or an int!

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

Code snippet from OperatorOverloadingAddSecondAttemptSpreadsheetCellTest.cpp

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, it calls the string 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 SpreadsheetCell to a string. You can prevent the implicit construction of a SpreadsheetCell from a string by marking that constructor with the explicit keyword:

image
class SpreadsheetCell
{
    public:
        SpreadsheetCell();
        SpreadsheetCell(double initialValue);
        explicit SpreadsheetCell(const string& initialValue);
        SpreadsheetCell(const SpreadsheetCell& src);
        SpreadsheetCell& operator=(const SpreadsheetCell& rhs);
    // Remainder omitted for brevity
};

Code snippet from OperatorOverloadingAddSecondAttemptSpreadsheetCell.h

The explicit keyword goes only in the class definition, and only makes sense when applied to constructors with exactly one argument.

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:

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

Code snippet from OperatorOverloadingAddSecondAttemptSpreadsheetCell.cpp

Note also that this demonstrates that you don’t need to create a variable to return a value.

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:

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

Code snippet from OperatorOverloadingAddSecondAttemptSpreadsheetCellTest.cpp

The implicit conversion works fine when the SpreadsheetCell object is on the left of the operator, but 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 the above code to work with an operator+ method.

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

image
const SpreadsheetCell operator+(const SpreadsheetCell& lhs,
    const SpreadsheetCell& rhs)
{
    SpreadsheetCell newCell;
    newCell.set(lhs.mValue + rhs.mValue); // update mValue and mString.
    return newCell;
}

Code snippet from OperatorOverloadingSpreadsheetCell.cpp

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

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

Code snippet from OperatorOverloadingSpreadsheetCellTest.cpp

Note that the implementation of the global operator+ accesses protected data members of SpreadsheetCell objects. Therefore, it must be a friend function of the SpreadsheetCell class:

image
class SpreadsheetCell
{
    public:
        // Omitted for brevity
        friend const SpreadsheetCell operator+(const SpreadsheetCell& lhs,
            const SpreadsheetCell& rhs);
        //Omitted for brevity
};

Code snippet from OperatorOverloadingSpreadsheetCell.h

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

image
aThirdCell = 4.5 + 5.5;

Code snippet from OperatorOverloadingSpreadsheetCellTest.cpp

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 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 declarations of -, *, and / (you can also overload %, but it doesn’t make sense for the double values stored in SpreadsheetCells):

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

Code snippet from OperatorOverloadingSpreadsheetCell.h

Here are the implementations. The only tricky aspect is remembering to check for division by zero. This implementation throws an exception if division by zero is detected:

image
const SpreadsheetCell operator-(const SpreadsheetCell& lhs,
    const SpreadsheetCell& rhs)
{
    SpreadsheetCell newCell;
    newCell.set(lhs.mValue - rhs.mValue); // update mValue and mString.
    return newCell;
}
const SpreadsheetCell operator*(const SpreadsheetCell& lhs,
    const SpreadsheetCell& rhs)
{
    SpreadsheetCell newCell;
    newCell.set(lhs.mValue * rhs.mValue); // update mValue and mString.
    return newCell;
}
const SpreadsheetCell operator/(const SpreadsheetCell& lhs,
    const SpreadsheetCell& rhs)
{
    if (rhs.mValue == 0)
        throw invalid_argument("Divide by zero.");
    SpreadsheetCell newCell;
    newCell.set(lhs.mValue / rhs.mValue); // update mValue and mString
    return newCell;
}

Code snippet from OperatorOverloadingSpreadsheetCell.cpp

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.

pen.gif

In C++, you cannot change the precedence of operators. For example, * and / are always evaluated before + and -. The only thing user-defined operators can do is specify the implementation once the precedence of operations has been determined.

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 provides operator+= also. 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 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:

image
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
};

Code snippet from OperatorOverloadingSpreadsheetCell.h

Here are the implementations:

image
SpreadsheetCell& SpreadsheetCell::operator+=(const SpreadsheetCell& rhs)
{
    set(mValue + rhs.mValue); // Call set to update mValue and mString.
    return *this;
}
SpreadsheetCell& SpreadsheetCell::operator-=(const SpreadsheetCell& rhs)
{
    set(mValue - rhs.mValue); // Call set to update mValue and mString.
    return *this;
}
SpreadsheetCell& SpreadsheetCell::operator*=(const SpreadsheetCell& rhs)
{
    set(mValue * rhs.mValue); // Call set to update mValue and mString.
    return *this;
}
SpreadsheetCell& SpreadsheetCell::operator/=(const SpreadsheetCell& rhs)
{
    if (rhs.mValue == 0)
        throw invalid_argument("Divide by zero.");
    set(mValue / rhs.mValue); // Call set to update mValue and mString.
    return *this;
}

Code snippet from OperatorOverloadingSpreadsheetCell.cpp

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

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

Code snippet from OperatorOverloadingSpreadsheetCellTest.cpp

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

5.4 += aThirdCell;

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 friend functions so that you can use implicit conversion on both the left-hand side and right-hand side of the operator. The comparison operators all return a bool. Of course, you can change the return type, but we don’t recommend it. Here are the declarations and definitions:

image
class SpreadsheetCell
{
    public:
        // Omitted for brevity
        friend bool operator==(const SpreadsheetCell& lhs,
            const SpreadsheetCell& rhs);
        friend bool operator<(const SpreadsheetCell& lhs,
            const SpreadsheetCell& rhs);
        friend bool operator>(const SpreadsheetCell& lhs,
            const SpreadsheetCell& rhs);
        friend bool operator!=(const SpreadsheetCell& lhs,
            const SpreadsheetCell& rhs);
        friend bool operator<=(const SpreadsheetCell& lhs,
            const SpreadsheetCell& rhs);
        friend bool operator>=(const SpreadsheetCell& lhs,
            const SpreadsheetCell& rhs);
        // Omitted for brevity
};

Code snippet from OperatorOverloadingSpreadsheetCell.h

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

Code snippet from OperatorOverloadingSpreadsheetCell.cpp

pen.gif

The preceding overloaded operators are working with mValue, which is a double. Most of the time, performing equality or inequality tests on floating point values is not a good idea. You should use an epsilon test, but this falls outside the scope of this book.

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 to doubles and ints:

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

Code snippet from OperatorOverloadingSpreadsheetCellTest.cpp

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().

pen.gif

Provide operator overloading as a service to clients of your class.

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 STL uses operator overloading extensively. Chapter 18 explains how and when to overload the rest of the operators. Chapters 11 to 17 cover the STL.

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 3 and 4. 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 protected or private and provide getter and setter methods for them. This is how the SpreadsheetCell class is implemented. mValue and mString are protected; set(), getValue(), and getString() set and retrieve those values. That way you can keep mValue and mString in synch internally without worrying about clients delving in and changing those values.

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 hacking. 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. 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.

To use this approach with the Spreadsheet class, simply rename the old Spreadsheet class to SpreadsheetImpl. Here is the new SpreadsheetImpl class (which is identical to the old Spreadsheet class, but with a different name):

image
#include "SpreadsheetCell.h"
class SpreadsheetApplication; // Forward declaration
class SpreadsheetImpl
{
    public:
        SpreadsheetImpl(const SpreadsheetApplication& theApp,
            int inWidth = kMaxWidth, int inHeight = kMaxHeight);
        SpreadsheetImpl(const SpreadsheetImpl& src);
        ~SpreadsheetImpl();
        SpreadsheetImpl &operator=(const SpreadsheetImpl& rhs);
        void setCellAt(int x, int y, const SpreadsheetCell& inCell);
        SpreadsheetCell getCellAt(int x, int y);
        int getId() const;
        static const int kMaxHeight = 100;
        static const int kMaxWidth = 100;
    protected:
        bool inRange(int val, int upper);
        void copyFrom(const SpreadsheetImpl& src);
        int mWidth, mHeight;
        int mId;
        SpreadsheetCell** mCells;
        const SpreadsheetApplication& mTheApp;
        static int sCounter;
};

Code snippet from SeparateImplSpreadsheetImpl.h

Then define a new Spreadsheet class that looks like this:

image
#include "SpreadsheetCell.h"
// Forward declarations
class SpreadsheetImpl;
class SpreadsheetApplication;
class Spreadsheet
{
    public:
        Spreadsheet(const SpreadsheetApplication& theApp, int inWidth,
            int inHeight);
        Spreadsheet(const SpreadsheetApplication& theApp);
        Spreadsheet(const Spreadsheet& src);
        ~Spreadsheet();
        Spreadsheet& operator=(const Spreadsheet& rhs);
        void setCellAt(int x, int y, const SpreadsheetCell& inCell);
        SpreadsheetCell getCellAt(int x, int y);
        int getId() const;
    protected:
        SpreadsheetImpl* mImpl;
};

Code snippet from SeparateImplSpreadsheet.h

This class now contains only one data member: a pointer to a SpreadsheetImpl. The public methods are identical to the old Spreadsheet with one exception: The Spreadsheet constructor with default arguments has been split into two constructors because the values for the default arguments were const members that are no longer in the Spreadsheet class. Instead, the SpreadsheetImpl class will provide the defaults.

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

image
void Spreadsheet::setCellAt(int x, int y, const SpreadsheetCell& inCell)
{
    mImpl->setCellAt(x, y, inCell);
}
SpreadsheetCell Spreadsheet::getCellAt(int x, int y)
{
    return mImpl->getCellAt(x, y);
}
int Spreadsheet::getId() const
{
    return mImpl->getId();
}

Code snippet from SeparateImplSpreadsheet.cpp

The constructors for the Spreadsheet must construct a new SpreadsheetImpl to do its work, and the destructor must free the dynamically allocated memory. Note that the SpreadsheetImpl class has only one constructor with default arguments. Both normal constructors in the Spreadsheet class call that constructor on the SpreadsheetImpl class:

image
Spreadsheet::Spreadsheet(const SpreadsheetApplication& theApp, int inWidth,
    int inHeight) 
{
    mImpl = new SpreadsheetImpl(theApp, inWidth, inHeight);
}
Spreadsheet::Spreadsheet(const SpreadsheetApplication& theApp) 
{
    mImpl = new SpreadsheetImpl(theApp);
}
Spreadsheet::Spreadsheet(const Spreadsheet& src)
{
    mImpl = new SpreadsheetImpl(*(src.mImpl));
}
Spreadsheet::~Spreadsheet()
{
    delete mImpl;
    mImpl = nullptr;
}

Code snippet from SeparateImplSpreadsheet.cpp

The copy constructor looks a bit strange because it needs to copy the underlying SpreadsheetImpl from the source spreadsheet. Because the copy constructor takes a reference to a SpreadsheetImpl, not a pointer, 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 SpreadsheetImpl:

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

Code snippet from SeparateImplSpreadsheet.cpp

The first line in the assignment operator looks a little strange. You might be tempted to write this line instead:

 mImpl = rhs.mImpl; // Incorrect assignment!

That code will compile and run, but it doesn’t do what you want. It just copies pointers so that the left-hand side and right-hand side Spreadsheets now both possess pointers to the same SpreadsheetImpl. If one of them changes it, the change will show up in the other. If one of them destroys it, the other will be left with a dangling pointer. Therefore, you can’t just assign the pointers. You must force the SpreadsheetImpl assignment operator to run, which only happens when you copy direct objects. By dereferencing the mImpl pointers, you force direct object assignment, which causes the assignment operator to be called. Note that you can only do this because you already allocated memory for mImpl in the constructor.

This technique to truly separate interface from implementation is powerful. Although 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 cost of a full rebuild if the implementation of the class changes. A full rebuild on a huge project might take hours. With stable interface classes, rebuild time is minimized, and concepts like precompiled headers can further reduce build costs. A discussion on precompiled headers is outside the scope of this book.

SUMMARY

This chapter, along with Chapter 6, 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 must free the memory in the destructor, copy the memory in the copy constructor, and both free and copy the memory in the assignment operator. You learned how to prevent assignment and pass-by-value by declaring a private copy constructor and assignment operator.

You learned 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 parameters. The 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 an 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 in Chapter 8.

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

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