15
Overloading C++ Operators

OVERVIEW OF OPERATOR OVERLOADING

As Chapter 1 explains, operators in C++ are symbols such as +, <, *, and <<. They work on built-in types such as int and double to allow you to perform arithmetic, logical, and other operations. There are also operators such as -> and * that allow you to dereference pointers. The concept of operators in C++ is broad, and even includes [] (array index), () (function call), casting, and the memory allocation and deallocation operators. Operator overloading allows you to change the behavior of language operators for your classes. However, this capability comes with rules, limitations, and choices.

Why Overload Operators?

Before learning how to overload operators, you probably want to know why you would ever want to do so. The reasons vary for the different operators, but the general guiding principle is to make your classes behave like built-in types. The closer your classes are to built-in types, the easier they will be for clients to use. For example, if you want to write a class to represent fractions, it’s quite helpful to have the ability to define what +, -, *, and / mean when applied to objects of that class.

Another reason to overload operators is to gain greater control over the behavior in your program. For example, you can overload memory allocation and deallocation operators for your classes to specify exactly how memory should be distributed and reclaimed for each new object.

It’s important to emphasize that operator overloading doesn’t necessarily make things easier for you as the class developer; its main purpose is to make things easier for users of the class.

Limitations to Operator Overloading

Here is a list of things you cannot do when you overload operators:

  • You cannot add new operator symbols. You can only redefine the meanings of operators already in the language. The table in the section, “Summary of Overloadable Operators,” lists all of the operators that you can overload.
  • There are a few operators that you cannot overload, such as . (member access in an object), :: (scope resolution operator), sizeof, ?: (the conditional operator), and a few others. The table lists all the operators that you can overload. The operators that you can’t overload are usually not those you would care to overload anyway, so you shouldn’t find this restriction limiting.
  • The arity describes the number of arguments, or operands, associated with the operator. You can only change the arity for the function call, new, and delete operators. For all other operators, you cannot change the arity. Unary operators, such as ++, work on only one operand. Binary operators, such as /, work on two operands. The main situation where this limitation might affect you is when overloading [] (array index), which is discussed later in this chapter.
  • You cannot change the precedence or associativity of the operator. These rules determine in which order operators are evaluated in a statement. Again, this constraint shouldn’t be cause for concern in most programs because there are rarely benefits to changing the order of evaluation.
  • You cannot redefine operators for built-in types. The operator must be a method in a class, or at least one of the arguments to a global overloaded operator function must be a user-defined type (for example, a class). This means that you can’t do something ridiculous, such as redefine + for ints to mean subtraction (though you could do so for your classes). The one exception to this rule is the memory allocation and deallocation operators; you can replace the global operators for all memory allocations in your program.

Some of the operators already mean two different things. For example, the operator can be used as a binary operator (as in x = y - z;) or as a unary operator (as in x = -y;). The * operator can be used for multiplication or for dereferencing a pointer. The << operator is the stream insertion operator or the left-shift operator, depending on the context. You can overload both meanings of operators with dual meanings.

Choices in Operator Overloading

When you overload an operator, you write a function or method with the name operatorX, where X is the symbol for some operator, and with optional white space between operator and X. For example, Chapter 9 declares operator+ for SpreadsheetCell objects like this:

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

The following sections describe several choices involved in each overloaded operator function or method you write.

Method or Global Function

First, you must decide whether your operator should be a method of your class or a global function (sometimes a friend of the class). How do you choose? First, you need to understand the difference between these two choices. When the operator is a method of a class, the left-hand side of the operator expression must always be an object of that class. If you write a global function, the left-hand side can be an object of a different type.

There are three different types of operators:

  • Operators that must be methods. The C++ language requires some operators to be methods of a class because they don’t make sense outside of a class. For example, operator= is tied so closely to the class that it can’t exist anywhere else. The table in the section, “Summary of Overloadable Operators,” lists those operators that must be methods. Most operators do not impose this requirement.
  • Operators that must be global functions. Whenever you need to allow the left-hand side of the operator to be a variable of a different type than your class, you must make the operator a global function. This rule applies specifically to operator<< and operator>>, where the left-hand side is an iostream object, not an object of your class. Additionally, commutative operators like binary + and should allow variables that are not objects of your class on the left-hand side. Chapter 9 discusses this problem.
  • Operators that can be either methods or global functions. There is some disagreement in the C++ community on whether it’s better to write methods or global functions to overload operators. However, I recommend the following rule: make every operator a method unless you must make it a global function, as described previously. One major advantage to this rule is that methods can be virtual, while global functions obviously cannot. Therefore, when you plan to write overloaded operators in an inheritance tree, you should make them methods if possible.

When you write an overloaded operator as a method, you should mark it const if it doesn’t change the object. That way, it can be called on const objects.

Choosing Argument Types

You are somewhat limited in your choice of argument types because, as stated earlier, for most operators you cannot change the number of arguments. For example, operator/ must always have two arguments if it is a global function, and one argument if it’s a method. The compiler issues an error if it differs from this standard. In this sense, the operator functions are different from normal functions, which you can overload with any number of parameters. Additionally, although you can write the operator for whichever types you want, the choice is usually constrained by the class for which you are writing the operator. For example, if you want to implement addition for class T, you don’t write an operator+ that takes two strings! The real choice arises when you try to determine whether to take parameters by value or by reference, and whether or not to make them const.

The choice of value versus reference is easy: you should take every non-primitive parameter type by reference. As Chapters 9 and 11 explain, you should never pass objects by value if you can pass-by-reference instead.

The const decision is also trivial: mark every parameter const unless you actually modify it. The table in the section, “Summary of Overloadable Operators,” shows sample prototypes for each operator, with the arguments marked const and reference as appropriate.

Choosing Return Types

C++ doesn’t determine overload resolution based on return type. Thus, you can specify any return type you want when you write overloaded operators. However, just because you can do something doesn’t mean you should do it. This flexibility implies that you could write confusing code in which comparison operators return pointers, and arithmetic operators return bools. However, you shouldn’t do that. Instead, you should write your overloaded operators such that they return the same types as the operators do for the built-in types. If you write a comparison operator, return a bool. If you write an arithmetic operator, return an object representing the result. Sometimes the return type is not obvious at first. For example, as Chapter 8 mentions, operator= should return a reference to the object on which it’s called in order to support nested assignments. Other operators have similarly tricky return types, all of which are summarized in the table in the section, “Summary of Overloadable Operators.”

The same choices of reference and const apply to return types as well. However, for return values, the choices are more difficult. The general rule for value or reference is to return a reference if you can; otherwise, return a value. How do you know when you can return a reference? This choice applies only to operators that return objects: the choice is moot for the comparison operators that return bool, the conversion operators that have no return type, and the function call operator, which may return any type you want. If your operator constructs a new object, then you must return that new object by value. If it does not construct a new object, you can return a reference to the object on which the operator is called, or one of its arguments. The table in the section, “Summary of Overloadable Operators,” shows examples.

A return value that can be modified as an lvalue (the left-hand side of an assignment expression) must be non-const. Otherwise, it should be const. More operators than you might expect require that you return lvalues, including all of the assignment operators (operator=, operator+=, operator-=, and so on).

Choosing Behavior

You can provide whichever implementation you want in an overloaded operator. For example, you could write an operator+ that launches a game of Scrabble. However, as Chapter 6 describes, you should generally constrain your implementations to provide behaviors that clients expect. Write operator+ so that it performs addition, or something like addition, such as string concatenation. This chapter explains how you should implement your overloaded operators. In exceptional circumstances, you might want to differ from these recommendations; but, in general, you should follow the standard patterns.

Operators You Shouldn’t Overload

Some operators should not be overloaded, even though it is permitted. Specifically, the address-of operator (operator&) is not particularly useful to overload, and leads to confusion if you do because you are changing fundamental language behavior (taking addresses of variables) in potentially unexpected ways. The entire Standard Library, which uses operator overloading extensively, never overloads the address-of operator.

Additionally, you should avoid overloading the binary Boolean operators operator&& and operator|| because you lose C++’s short-circuit evaluation rules.

Finally, you should not overload the comma operator (operator,). Yes, you read that correctly: there really is a comma operator in C++. It’s also called the sequencing operator, and is used to separate two expressions in a single statement, while guaranteeing that they are evaluated left to right. There is rarely a good reason to overload this operator.

Summary of Overloadable Operators

The following table lists the operators that you can overload, specifies whether they should be methods of the class or global functions, summarizes when you should (or should not) overload them, and provides sample prototypes showing the proper return values.

This table is a useful reference for the future when you want to write an overloaded operator. You’re bound to forget which return type you should use, and whether or not the function should be a method.

In this table, T is the name of the class for which the overloaded operator is written, and E is a different type. Note that the sample prototypes given are not exhaustive; often there are other combinations of T and E possible for a given operator.

OPERATOR NAME OR CATEGORY METHOD OR GLOBAL FUNCTION WHEN TO OVERLOAD SAMPLE PROTOTYPE
operator+
operator-
operator*
operator/
operator%
Binary arithmetic Global function recommended Whenever you want to provide these operations for your class T operator+(const T&, const T&);
T operator+(const T&, const E&);
operator-
operator+
operator~
Unary arithmetic and bitwise operators Method recommended Whenever you want to provide these operations for your class T operator-() const;
operator++
operator--
Pre-increment and pre-decrement Method recommended Whenever you overload += and -= taking an arithmetic argument (int, long, ...) T& operator++();
operator++
operator--
Post-increment and post-decrement Method recommended Whenever you overload += and -= taking an arithmetic argument (int, long, ...) T operator++(int);
operator= Assignment operator Method required Whenever your class has dynamically allocated memory or resources, or members that are references T& operator=(const T&);
operator+=
operator-=
operator*=
operator/=
operator%=
Shorthand arithmetic operator assignments Method recommended Whenever you overload the binary arithmetic operators and your class is not designed to be immutable T& operator+=(const T&);
T& operator+=(const E&);
operator<<
operator>>
operator&
operator|
operator^
Binary bitwise operators Global function recommended Whenever you want to provide these operations T operator<<(const T&, const T&);
T operator<<(const T&, const E&);
operator<<=
operator>>=
operator&=
operator|=
operator^=
Shorthand bitwise operator assignments Method recommended Whenever you overload the binary bitwise operators and your class is not designed to be immutable T& operator<<=(const T&);
T& operator<<=(const E&);
operator<
operator>
operator<=
operator>=
operator==
operator!=
Binary comparison operators Global function recommended Whenever you want to provide these operations bool operator<(const T&, const T&);
bool operator<(const T&, const E&);
operator<<
operator>>
I/O stream operators (insertion and extraction) Global function required Whenever you want to provide these operations ostream& operator<<(ostream&, const T&);
istream& operator>>(istream&, T&);
operator! Boolean negation operator Member function recommended Rarely; use bool or void* conversion instead. bool operator!() const;
operator&&
operator||
Binary Boolean operators Global function recommended Rarely, if ever, because you lose short-circuiting; it’s better to overload & and | instead, as these never short-circuit. bool operator&&(const T&, const T&);
operator[] Subscripting (array index) operator Method required When you want to support subscripting E& operator[](size_t);
const E& operator[](size_t) const;
operator() Function call operator Method required When you want objects to behave like function pointers, or for multi-dimensional array access, since [] can only have one index Return type and parameters can vary; see further examples in this chapter.
operator type() Conversion, or cast, operators (separate operator for each type) Method required When you want to provide conversions from your class to other types operator double() const;
operator new
operator new[]
Memory allocation routines Method recommended When you want to control memory allocation for your classes (rarely) void* operator new(size_t size);
void* operator new[](size_t size);
operator delete
operator delete[]
Memory deallocation routines Method recommended Whenever you overload the memory allocation routines (rarely) void operator delete(void* ptr) noexcept;
void operator delete[](void* ptr) noexcept;
operator*
operator->
Dereferencing operators Method recommended for operator*
Method required for operator->
Useful for smart pointers E& operator*() const;
E* operator->() const;
operator& Address-of operator N/A Never N/A
operator->* Dereference pointer-to-member N/A Never N/A
operator, Comma operator N/A Never N/A

Rvalue References

Chapter 9 discusses rvalue references, written as && instead of the normal lvalue references, &. They are demonstrated in Chapter 9 by defining move assignment operators, which are used by the compiler in cases where the second object is a temporary object that will be destroyed after the assignment. The normal assignment operator from the preceding table has the following prototype:

T& operator=(const T&);

The move assignment operator has almost the same prototype, but uses an rvalue reference. It modifies the argument so it cannot be passed as const. See Chapter 9 for details.

T& operator=(T&&);

The preceding table does not include sample prototypes with rvalue reference semantics. However, for most operators it can make sense to write both a version using normal lvalue references and a version using rvalue references. Whether it makes sense depends on implementation details of your class. The operator= is one example from Chapter 9. Another example is operator+ to prevent unnecessary memory allocations. The std::string class from the Standard Library, for example, implements an operator+ using rvalue references as follows (simplified):

string operator+(string&& lhs, string&& rhs);

The implementation of this operator reuses memory of one of the arguments because they are being passed as rvalue references, meaning both are temporary objects that will be destroyed when this operator+ is finished. The implementation of the preceding operator+ has the following effect depending on the size and the capacity of both operands:

return std::move(lhs.append(rhs));

or

return std::move(rhs.insert(0, lhs));

In fact, std::string defines several overloaded operator+ operators with different combinations of lvalue references and rvalue references. The following is a list of all operator+ operators for std::string accepting two strings as arguments (simplified):

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

Reusing memory of one of the rvalue reference arguments is implemented in the same way as it is explained for move assignment operators in Chapter 9.

Relational Operators

There is a handy <utility> header file included with the C++ Standard Library. It contains quite a few helper functions and classes. It also contains the following set of function templates for relational operators in the std::rel_ops namespace:

template<class T> bool operator!=(const T& a, const T& b);// Needs operator==
template<class T> bool operator>(const T& a, const T& b); // Needs operator<
template<class T> bool operator<=(const T& a, const T& b);// Needs operator<
template<class T> bool operator>=(const T& a, const T& b);// Needs operator<

These function templates define the operators !=, >, <=, and >= in terms of the == and < operators for any class. If you implement operator== and operator< in your class, you get the other relational operators for free with these templates. You can make these available for your class by simply adding a #include <utility> and adding the following using statement:

using namespace std::rel_ops;

However, one problem with this technique is that now those operators might be created for all classes that you use in relational operations, not only for your own class.

A second problem with this technique is that utility templates such as std::greater<T> (discussed in Chapter 18) do not work with those automatically generated relational operators.

Yet another problem with these is that implicit conversions will not work. Therefore:

OVERLOADING THE ARITHMETIC OPERATORS

Chapter 9 shows how to write the binary arithmetic operators and the shorthand arithmetic assignment operators, but it does not cover how to overload the other arithmetic operators.

Overloading Unary Minus and Unary Plus

C++ has several unary arithmetic operators. Two of these are unary minus and unary plus. Here is an example of these operators using ints:

int i, j = 4;
i = -j;    // Unary minus
i = +i;    // Unary plus
j = +(-i); // Apply unary plus to the result of applying unary minus to i.
j = -(-i); // Apply unary minus to the result of applying unary minus to i.

Unary minus negates the operand, while unary plus returns the operand directly. Note that you can apply unary plus or unary minus to the result of unary plus or unary minus. These operators don’t change the object on which they are called so you should make them const.

Here is an example of a unary operator- as a member function for a SpreadsheetCell class. Unary plus is usually an identity operation, so this class doesn’t overload it:

SpreadsheetCell SpreadsheetCell::operator-() const
{
    return SpreadsheetCell(-getValue());
}

operator- doesn’t change the operand, so this method must construct a new SpreadsheetCell with the negated value, and return it. Thus, it can’t return a reference. You can use this operator as follows:

SpreadsheetCell c1(4);
SpreadsheetCell c3 = -c1;

Overloading Increment and Decrement

There are four ways to add 1 to a variable:

i = i + 1;
i += 1;
++i;
i++;

The last two forms are called the increment operators. The first form is prefix increment, which adds 1 to the variable, then returns the newly incremented value for use in the rest of the expression. The second form is postfix increment, which returns the old (non-incremented) value for use in the rest of the expression. The decrement operators work similarly.

The two possible meanings for operator++ and operator-- (prefix and postfix) present a problem when you want to overload them. When you write an overloaded operator++, for example, how do you specify whether you are overloading the prefix or the postfix version? C++ introduced a hack to allow you to make this distinction: the prefix versions of operator++ and operator-- take no arguments, while the postfix versions take one unused argument of type int.

The prototypes of these overloaded operators for the SpreadsheetCell class look like this:

SpreadsheetCell& operator++();   // Prefix
SpreadsheetCell operator++(int); // Postfix
SpreadsheetCell& operator--();   // Prefix
SpreadsheetCell operator--(int); // Postfix

The return value in the prefix forms is the same as the end value of the operand, so prefix increment and decrement can return a reference to the object on which they are called. The postfix versions of increment and decrement, however, return values that are different from the end values of the operands, so they cannot return references.

Here are the implementations for operator++:

SpreadsheetCell& SpreadsheetCell::operator++()
{
    set(getValue() + 1);
    return *this;
}

SpreadsheetCell SpreadsheetCell::operator++(int)
{
    auto oldCell(*this); // Save current value
    ++(*this);           // Increment using prefix ++
    return oldCell;      // Return the old value
}

The implementations for operator-- are almost identical. Now you can increment and decrement SpreadsheetCell objects to your heart’s content:

SpreadsheetCell c1(4);
SpreadsheetCell c2(4);
c1++;
++c2;

Increment and decrement also work on pointers. When you write classes that are smart pointers or iterators, you can overload operator++ and operator-- to provide pointer incrementing and decrementing.

OVERLOADING THE BITWISE AND BINARY LOGICAL OPERATORS

The bitwise operators are similar to the arithmetic operators, and the bitwise shorthand assignment operators are similar to the arithmetic shorthand assignment operators. However, they are significantly less common, so no examples are shown here. The table in the section “Summary of Overloadable Operators” shows sample prototypes, so you should be able to implement them easily if the need ever arises.

The logical operators are trickier. It’s not recommended to overload && and ||. These operators don’t really apply to individual types: they aggregate results of Boolean expressions. Additionally, you lose the short-circuit evaluation, because both the left-hand side and the right-hand side have to be evaluated before they can be bound to the parameters of your overloaded operator && and ||. Thus, it rarely, if ever, makes sense to overload them for specific types.

OVERLOADING THE INSERTION AND EXTRACTION OPERATORS

In C++, you use operators not only for arithmetic operations, but also for reading from, and writing to, streams. For example, when you write ints and strings to cout, you use the insertion operator <<:

int number = 10;
cout << "The number is " << number << endl;

When you read from streams, you use the extraction operator >>:

int number;
string str;
cin >> number >> str;

You can write insertion and extraction operators that work on your classes as well, so that you can read and write them like this:

SpreadsheetCell myCell, anotherCell, aThirdCell;
cin >> myCell >> anotherCell >> aThirdCell;
cout << myCell << " " << anotherCell << " " << aThirdCell << endl;

Before you write the insertion and extraction operators, you need to decide how you want to stream your class out and how you want to read it in. In this example, the SpreadsheetCells simply read and write double values.

The object on the left of an extraction or insertion operator is an istream or ostream (such as cin or cout), not a SpreadsheetCell object. Because you can’t add a method to the istream or ostream classes, you must write the extraction and insertion operators as global functions. The declaration of these functions looks like this:

class SpreadsheetCell
{
    // Omitted for brevity
};
std::ostream& operator<<(std::ostream& ostr, const SpreadsheetCell& cell);
std::istream& operator>>(std::istream& istr, SpreadsheetCell& cell);

By making the insertion operator take a reference to an ostream as its first parameter, you allow it to be used for file output streams, string output streams, cout, cerr, and clog. See Chapter 13 for details on streams. Similarly, by making the extraction operator take a reference to an istream, you make it work on file input streams, string input streams, and cin.

The second parameter to operator<< and operator>> is a reference to the SpreadsheetCell object that you want to write or read. The insertion operator doesn’t change the SpreadsheetCell it writes, so that reference can be const. The extraction operator, however, modifies the SpreadsheetCell object, requiring the argument to be a non-const reference.

Both operators return a reference to the stream they were given as their first argument so that calls to the operator can be nested. Remember that the operator syntax is shorthand for calling the global operator>> or operator<< functions explicitly. Consider this line:

cin >> myCell >> anotherCell >> aThirdCell;

It’s actually shorthand for this line:

operator>>(operator>>(operator>>(cin, myCell), anotherCell), aThirdCell);

As you can see, the return value of the first call to operator>> is used as input to the next call. Thus, you must return the stream reference so that it can be used in the next nested call. Otherwise, the nesting won’t compile.

Here are the implementations for operator<< and operator>> for the SpreadsheetCell class:

ostream& operator<<(ostream& ostr, const SpreadsheetCell& cell)
{
    ostr << cell.getValue();
    return ostr;
}

istream& operator>>(istream& istr, SpreadsheetCell& cell)
{
    double value;
    istr >> value;
    cell.set(value);
    return istr;
}

OVERLOADING THE SUBSCRIPTING OPERATOR

Pretend for a few minutes that you have never heard of the vector or array class templates in the Standard Library, and so you have decided to write your own dynamically allocated array class. This class would allow you to set and retrieve elements at specified indices, and would take care of all memory allocation “behind the scenes.” A first stab at the class definition for a dynamically allocated array might look like this:

template <typename T>
class Array
{
    public:
         // Creates an array with a default size that will grow as needed.
        Array();
        virtual ~Array();

         // Disallow assignment and pass-by-value
        Array<T>& operator=(const Array<T>& rhs) = delete;
        Array(const Array<T>& src) = delete;

        // Returns the value at index x. Throws an exception of type
        // out_of_range if index x does not exist in the array.
        const T& getElementAt(size_t x) const;

        // Sets the value at index x. If index x is out of range,
        // allocates more space to make it in range.
        void setElementAt(size_t x, const T& value);

        size_t getSize() const;
    private:
        static const size_t kAllocSize = 4;
        void resize(size_t newSize);
        T* mElements = nullptr;
        size_t mSize = 0;
};

The interface supports setting and accessing elements. It provides random-access guarantees: a client could create an array and set elements 1, 100, and 1000 without worrying about memory management.

Here are the implementations of the methods:

template <typename T> Array<T>::Array()
{
    mSize = kAllocSize;
    mElements = new T[mSize] {}; // Elements are zero-initialized! 
}

template <typename T> Array<T>::~Array()
{
    delete [] mElements;
    mElements = nullptr;
}

template <typename T> void Array<T>::resize(size_t newSize)
{
    // Create new bigger array with zero-initialized elements.
    auto newArray = std::make_unique<T[]>(newSize);

    // The new size is always bigger than the old size (mSize)
    for (size_t i = 0; i < mSize; i++) {
        // Copy the elements from the old array to the new one
        newArray[i] = mElements[i];
    }

    // Delete the old array, and set the new array
    delete[] mElements;
    mSize = newSize;
    mElements = newArray.release();
}

template <typename T> const T& Array<T>::getElementAt(size_t x) const
{
    if (x >= mSize) {
        throw std::out_of_range("");
    }
    return mElements[x];
}

template <typename T> void Array<T>::setElementAt(size_t x, const T& val)
{
    if (x >= mSize) {
         // Allocate kAllocSize past the element the client wants
        resize(x + kAllocSize);
    }
    mElements[x] = val;
}

template <typename T> size_t Array<T>::getSize() const
{
    return mSize;
}

Pay some attention to the exception-safe implementation of the resize() method. First, it creates a new array of appropriate size, and stores it in a unique_ptr. Then, all elements are copied from the old array to the new array. If anything goes wrong while copying the values, the unique_ptr cleans up the memory automatically. Finally, when both the allocation of the new array and copying all the elements was successful, that is, no exceptions have been thrown, only then we delete the old mElements array and assign the new array to it. The last line has to use release() to release the ownership of the new array from the unique_ptr, otherwise the array will get destroyed when the destructor for the unique_ptr is called.

Here is a small example of how you could use this class:

Array<int> myArray;
for (size_t i = 0; i < 10; i++) {
    myArray.setElementAt(i, 100);
}
for (size_t i = 0; i < 10; i++) {
    cout << myArray.getElementAt(i) << " ";
}

As you can see, you never have to tell the array how much space you need. It allocates as much space as it requires to store the elements you give it. However, it’s inconvenient to always have to use the setElementAt() and getElementAt() methods. It would be nice to be able to use conventional array index notation like this:

Array<int> myArray;
for (size_t i = 0; i < 10; i++) {
    myArray[i] = 100;
}
for (size_t i = 0; i < 10; i++) {
    cout << myArray[i] << " ";
}

This is where the overloaded subscripting operator comes in. You can add an operator[] to the class with the following implementation:

template <typename T> T& Array<T>::operator[](size_t x)
{
    if (x >= mSize) {
         // Allocate kAllocSize past the element the client wants.
        resize(x + kAllocSize);
    }
    return mElements[x];
}

The example code using array index notation now compiles. The operator[] can be used to both set and get elements because it returns a reference to the element at location x. This reference can be used to assign to that element. When operator[] is used on the left-hand side of an assignment statement, the assignment actually changes the value at location x in the mElements array.

Providing Read-Only Access with operator[]

Although it’s sometimes convenient for operator[] to return an element that can serve as an lvalue, you don’t always want that behavior. It would be nice to be able to provide read-only access to the elements of the array as well, by returning a const reference. To provide for this, you need two operator[]s: one returns a reference and one returns a const reference:

T& operator[](size_t x);
const T& operator[](size_t x) const;

Remember that you can’t overload a method or operator based only on the return type, so the second overload returns a const reference and is marked as const.

Here is the implementation of the const operator[]. It throws an exception if the index is out of range instead of trying to allocate new space. It doesn’t make sense to allocate new space when you’re only trying to read the element value.

template <typename T> const T& Array<T>::operator[](size_t x) const
{
    if (x >= mSize) {
        throw std::out_of_range("");
    }
    return mElements[x];
}

The following code demonstrates these two forms of operator[]:

void printArray(const Array<int>& arr)
{
    for (size_t i = 0; i < arr.getSize(); i++) {
        cout << arr[i] << " "; // Calls the const operator[] because arr is
                               // a const object.
    }
    cout << endl;
}

int main()
{
    Array<int> myArray;
    for (size_t i = 0; i < 10; i++) {
        myArray[i] = 100; // Calls the non-const operator[] because
                          // myArray is a non-const object.
    }
    printArray(myArray);
    return 0;
}

Note that the const operator[] is called in printArray() only because arr is const. If arr were not const, the non-const operator[] would be called, despite the fact that the result is not modified.

The const operator[] is called for const objects, so it cannot grow the size of the array. The current implementation throws an exception when the given index is out of bounds. An alternative would be to return a zero-initialized element instead of throwing. This can be done as follows:

template <typename T> const T& Array<T>::operator[](size_t x) const
{
    if (x >= mSize) {
        static T nullValue = T();
        return nullValue;
    }
    return mElements[x];
}

The nullValue static variable is initialized using the zero-initialization1 syntax T(). It’s up to you and your specific use case whether you opt for the throwing version or the version returning a null value.

Non-integral Array Indices

It is a natural extension of the paradigm of “indexing” into a collection to provide a key of some sort; a vector (or in general, any linear array) is a special case where the “key” is just a position in the array. Think of the argument of operator[] as providing a mapping between two domains: the domain of keys and the domain of values. Thus, you can write an operator[] that uses any type as its index. This type does not need to be an integer type. This is done for the Standard Library associative containers, like std::map, which are discussed in Chapter 17.

For example, you could create an associative array in which you use string keys instead of integers. Here is the definition for such an associative array class:

template <typename T>
class AssociativeArray
{
    public:
        virtual ~AssociativeArray() = default;

        T& operator[](std::string_view key);
        const T& operator[](std::string_view key) const;
    private:
        // Implementation details omitted
};

Implementing this class would be a good exercise for you. You can also find an implementation of this class in the downloadable source code for this book at www.wrox.com/go/proc++4e.

OVERLOADING THE FUNCTION CALL OPERATOR

C++ allows you to overload the function call operator, written as operator(). If you write an operator() for your class, you can use objects of that class as if they were function pointers. An object of a class with a function call operator is called a function object, or functor, for short. You can overload this operator only as a non-static method in a class. Here is an example of a simple class with an overloaded operator() and a class method with the same behavior:

class FunctionObject
{
    public:
        int operator() (int param); // Function call operator
        int doSquare(int param);    // Normal method
};

// Implementation of overloaded function call operator
int FunctionObject::operator() (int param)
{
    return doSquare(param);
}

// Implementation of normal method
int FunctionObject::doSquare(int param)
{
    return param * param;
}

Here is an example of code that uses the function call operator, contrasted with the call to a normal method of the class:

int x = 3, xSquared, xSquaredAgain;
FunctionObject square;
xSquared = square(x);               // Call the function call operator
xSquaredAgain = square.doSquare(x); // Call the normal method

At first, the function call operator probably seems a little strange. Why would you want to write a special method for a class to make objects of the class look like function pointers? Why wouldn’t you just write a function or a standard method of a class? The advantage of function objects over standard methods of objects is simple: these objects can sometimes masquerade as function pointers, that is, you can pass function objects as callback functions to other functions. This is discussed in more detail in Chapter 18.

The advantages of function objects over global functions are more intricate. There are two main benefits:

  • Objects can retain information in their data members between repeated calls to their function call operators. For example, a function object might be used to keep a running sum of numbers collected from each call to the function call operator.
  • You can customize the behavior of a function object by setting data members. For example, you could write a function object to compare an argument to the function call operator against a data member. This data member could be configurable so that the object could be customized for whatever comparison you want.

Of course, you could implement either of the preceding benefits with global or static variables. However, function objects provide a cleaner way to do it, and using global or static variables might cause problems in a multithreaded application. The true benefits of function objects are demonstrated with the Standard Library in Chapter 18.

By following the normal method overloading rules, you can write as many operator()s for your classes as you want. For example, you could add an operator() to the FunctionObject class that takes an std::string_view:

int operator() (int param);
void operator() (std::string_view str);

The function call operator can also be used to provide subscripting for multi-dimensional arrays. Simply write an operator() that behaves like operator[] but allows for more than one index. The only minor annoyance with this technique is that you have to use () to index instead of [], as in myArray(3, 4) = 6;.

OVERLOADING THE DEREFERENCING OPERATORS

You can overload three dereferencing operators: *, ->, and ->*. Ignoring ->* for the moment (I’ll come back to it later), consider the built-in meanings of * and ->. The * operator dereferences a pointer to give you direct access to its value, while -> is shorthand for a * dereference followed by a . member selection. The following code shows the equivalences:

SpreadsheetCell* cell = new SpreadsheetCell;
(*cell).set(5); // Dereference plus member selection
cell->set(5);   // Shorthand arrow dereference and member selection together

You can overload the dereferencing operators for your classes in order to make objects of the classes behave like pointers. The main use of this capability is for implementing smart pointers, introduced in Chapter 1. It is also useful for iterators, which the Standard Library uses extensively. Iterators are discussed in Chapter 17. This chapter teaches you the basic mechanics for overloading the relevant operators in the context of a simple smart pointer class template.

Here is the example smart pointer class template definition, without the dereferencing operators filled in yet:

template <typename T> class Pointer
{
    public:
        Pointer(T* ptr);
        virtual ~Pointer();

        // Prevent assignment and pass by value.
        Pointer(const Pointer<T>& src) = delete;
        Pointer<T>& operator=(const Pointer<T>& rhs) = delete;

        // Dereferencing operators will go here.
    private:
        T* mPtr = nullptr;
};

This smart pointer is about as simple as you can get. All it does is store a dumb raw pointer, and the storage pointed to by the pointer is deleted when the smart pointer is destroyed. The implementation is equally simple: the constructor takes a raw pointer, which is stored as the only data member in the class. The destructor frees the storage referenced by the pointer.

template <typename T> Pointer<T>::Pointer(T* ptr) : mPtr(ptr)
{
}

template <typename T> Pointer<T>::~Pointer()
{
    delete mPtr;
    mPtr = nullptr;
}

You want to be able to use the smart pointer class template like this:

Pointer<int> smartInt(new int);
*smartInt = 5; // Dereference the smart pointer.
cout << *smartInt << endl;

Pointer<SpreadsheetCell> smartCell(new SpreadsheetCell);
smartCell->set(5); // Dereference and member select the set method.
cout << smartCell->getValue() << endl;

As you can see from this example, you have to provide implementations of operator* and operator-> for this class. These are implemented in the next two sections.

Implementing operator*

When you dereference a pointer, you expect to be able to access the memory to which the pointer points. If that memory contains a simple type such as an int, you should be able to change its value directly. If the memory contains a more complicated type, such as an object, you should be able to access its data members or methods with the . operator.

To provide these semantics, you should return a reference from operator*. In the Pointer class, the declaration and definition are as follows:

template <typename T> class Pointer
{
    public:
        // Omitted for brevity
        T& operator*();
        const T& operator*() const;
        // Omitted for brevity
};

template <typename T> T& Pointer<T>::operator*()
{
    return *mPtr;
}

template <typename T> const T& Pointer<T>::operator*() const
{
    return *mPtr;
}

As you can see, operator* returns a reference to the object or variable to which the underlying raw pointer points. As with overloading the subscripting operators, it’s useful to provide both const and non-const versions of the method, which return a const reference and a non-const reference, respectively.

Implementing operator–>

The arrow operator is a bit trickier. The result of applying the arrow operator should be a member or method of an object. However, in order to implement it like that, you would have to be able to implement the equivalent of operator* followed by operator.; C++ doesn’t allow you to overload operator. for good reason: it’s impossible to write a single prototype that allows you to capture any possible member or method selection. Therefore, C++ treats operator-> as a special case. Consider this line:

smartCell->set(5);

C++ translates this to

(smartCell.operator->())->set(5);

As you can see, C++ applies another operator-> to whatever you return from your overloaded operator->. Therefore, you must return a pointer, like this:

template <typename T> class Pointer
{
    public:
        // Omitted for brevity
        T* operator->();
        const T* operator->() const;
        // Omitted for brevity
};

template <typename T> T* Pointer<T>::operator->()
{
    return mPtr;
}

template <typename T> const T* Pointer<T>::operator->() const
{
    return mPtr;
}

You may find it confusing that operator* and operator-> are asymmetric, but once you see them a few times, you’ll get used to it.

What in the World Are operator.* and operator–>*?

It’s perfectly legitimate in C++ to take the addresses of class data members and methods in order to obtain pointers to them. However, you can’t access a non-static data member or call a non-static method without an object. The whole point of class data members and methods is that they exist on a per-object basis. Thus, when you want to call the method or access the data member via the pointer, you must dereference the pointer in the context of an object. The following example demonstrates this. Chapter 11 discusses the syntactical details in the section “Pointers to Methods and Data Members.”

SpreadsheetCell myCell;
double (SpreadsheetCell::*methodPtr) () const = &SpreadsheetCell::getValue;
cout << (myCell.*methodPtr)() << endl;

Note the use of the .* operator to dereference the method pointer and call the method. There is also an equivalent operator->* for calling methods via pointers when you have a pointer to an object instead of the object itself. The operator looks like this:

SpreadsheetCell* myCell = new SpreadsheetCell();
double (SpreadsheetCell::*methodPtr) () const = &SpreadsheetCell::getValue;
cout << (myCell->*methodPtr)() << endl;

C++ does not allow you to overload operator.* (just as you can’t overload operator.), but you could overload operator->*. However, it is very tricky, and, given that most C++ programmers don’t even know that you can access methods and data members through pointers, it’s probably not worth the trouble. The std::shared_ptr template in the Standard Library, for example, does not overload operator->*.

WRITING CONVERSION OPERATORS

Going back to the SpreadsheetCell example, consider these two lines of code:

SpreadsheetCell cell(1.23);
double d1 = cell; // DOES NOT COMPILE!

A SpreadsheetCell contains a double representation, so it seems logical that you could assign it to a double variable. Well, you can’t. The compiler tells you that it doesn’t know how to convert a SpreadsheetCell to a double. You might be tempted to try forcing the compiler to do what you want, like this:

double d1 = (double)cell; // STILL DOES NOT COMPILE!

First, the preceding code still doesn’t compile because the compiler still doesn’t know how to convert the SpreadsheetCell to a double. It already knew from the first line what you wanted it to do, and it would do it if it could. Second, it’s a bad idea in general to add gratuitous casts to your program.

If you want to allow this kind of assignment, you must tell the compiler how to perform it. Specifically, you can write a conversion operator to convert SpreadsheetCells to doubles. The prototype looks like this:

operator double() const;

The name of the function is operator double. It has no return type because the return type is specified by the name of the operator: double. It is const because it doesn’t change the object on which it is called. The implementation looks like this:

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

That’s all you need to do to write a conversion operator from SpreadsheetCell to double. Now the compiler accepts the following lines and does the right thing at run time:

SpreadsheetCell cell(1.23);
double d1 = cell; // Works as expected

You can write conversion operators for any type with this same syntax. For example, here is an std::string conversion operator for SpreadsheetCell:

SpreadsheetCell::operator std::string() const
{
    return doubleToString(getValue());
}

Now you can write code like the following:

SpreadsheetCell cell(1.23);
string str = cell;

Solving Ambiguity Problems with Explicit Conversion Operators

Note that writing the double conversion operator for the SpreadsheetCell object introduces an ambiguity problem. Consider this line:

SpreadsheetCell cell(1.23);
double d2 = cell + 3.3; // DOES NOT COMPILE IF YOU DEFINE operator double()

This line now fails to compile. It worked before you wrote operator double(), so what’s the problem now? The issue is that the compiler doesn’t know if it should convert cell to a double with operator double() and perform double addition, or convert 3.3 to a SpreadsheetCell with the double constructor and perform SpreadsheetCell addition. Before you wrote operator double(), the compiler had only one choice: convert 3.3 to a SpreadsheetCell with the double constructor and perform SpreadsheetCell addition. However, now the compiler could do either. It doesn’t want to make a choice you might not like, so it refuses to make any choice at all.

The usual pre-C++11 solution to this conundrum is to make the constructor in question explicit, so that the automatic conversion using that constructor is prevented (see Chapter 9). However, you don’t want that constructor to be explicit because you generally like the automatic conversion of doubles to SpreadsheetCells. Since C++11, you can solve this problem by making the double conversion operator explicit instead of the constructor:

explicit operator double() const;

The following code demonstrates its use:

SpreadsheetCell cell = 6.6;                   // [1]
string str = cell;                            // [2]
double d1 = static_cast<double>(cell);        // [3]
double d2 = static_cast<double>(cell + 3.3);  // [4]

Here is what each line of code means:

  • [1] Uses the implicit conversion from a double to a SpreadsheetCell. Because this is in the declaration, this is done by calling the constructor that accepts a double.
  • [2] Uses the operator string() conversion operator.
  • [3] Uses the operator double() conversion operator. Note that because this conversion operator is now declared explicit, the cast is required.
  • [4] Uses the implicit conversion of 3.3 to a SpreadsheetCell, followed by operator+ on two SpreadsheetCells, followed by a required explicit cast to invoke operator double().

Conversions for Boolean Expressions

Sometimes it is useful to be able to use objects in Boolean expressions. For example, programmers often use pointers in conditional statements like this:

if (ptr != nullptr) { /* Perform some dereferencing action. */ }

Sometimes they write shorthand conditions such as this:

if (ptr) { /* Perform some dereferencing action. */ }

Other times, you see code as follows:

if (!ptr) { /* Do something. */ }

Currently, none of the preceding expressions compile with the Pointer smart pointer class template defined earlier. However, you can add a conversion operator to the class to convert it to a pointer type. Then, the comparisons to nullptr, as well as the object alone in an if statement, will trigger the conversion to the pointer type. The usual pointer type for the conversion operator is void* because that’s a pointer type with which you cannot do much except test it in Boolean expressions. Here is the implementation:

template <typename T> Pointer<T>::operator void*() const
{
    return mPtr;
}

Now the following code compiles and does what you expect:

void process(Pointer<SpreadsheetCell>& p)
{
    if (p != nullptr) { cout << "not nullptr" << endl; }
    if (p != NULL) { cout << "not NULL" << endl; }
    if (p) { cout << "not nullptr" << endl; }
    if (!p) { cout << "nullptr" << endl; }
}

int main()
{
    Pointer<SpreadsheetCell> smartCell(nullptr);
    process(smartCell);
    cout << endl;

    Pointer<SpreadsheetCell> anotherSmartCell(new SpreadsheetCell(5.0));
    process(anotherSmartCell);
}

The output is as follows:

nullptr

not nullptr
not NULL
not nullptr

Another alternative is to overload operator bool() as follows instead of operator void*(). After all, you’re using the object in a Boolean expression; why not convert it directly to a bool?

template <typename T> Pointer<T>::operator bool() const
{
    return mPtr != nullptr;
}

The following comparisons still work:

if (p != NULL) { cout << "not NULL" << endl; }
if (p) { cout << "not nullptr" << endl; }
if (!p) { cout << "nullptr" << endl; }

However, with operator bool(), the following comparison with nullptr results in a compilation error:

if (p != nullptr) { cout << "not nullptr" << endl; }  // Error

This is correct behavior because nullptr has its own type called nullptr_t, which is not automatically converted to the integer 0 (false). The compiler cannot find an operator!= that takes a Pointer object and a nullptr_t object. You could implement such an operator!= as a friend of the Pointer class:

template <typename T>
class Pointer
{
    public:
        // Omitted for brevity
        template <typename T>
        friend bool operator!=(const Pointer<T>& lhs, std::nullptr_t rhs);
        // Omitted for brevity
};

template <typename T>
bool operator!=(const Pointer<T>& lhs, std::nullptr_t rhs)
{
    return lhs.mPtr != rhs;
}

However, after implementing this operator!=, the following comparison stops working, because the compiler no longer knows which operator!= to use.

if (p != NULL) { cout << "not NULL" << endl; }

From this example, you might conclude that the operator bool() technique only seems appropriate for objects that don’t represent pointers and for which conversion to a pointer type really doesn’t make sense. Unfortunately, adding a conversion operator to bool presents some other unanticipated consequences. C++ applies “promotion” rules to silently convert bool to int whenever the opportunity arises. Therefore, with the operator bool(), the following code compiles and runs:

Pointer<SpreadsheetCell> anotherSmartCell(new SpreadsheetCell(5.0));
int i = anotherSmartCell; // Converts Pointer to bool to int.

That’s usually not behavior that you expect or desire. To prevent such assignments, you could explicitly delete the conversion operators to int, long, long long, and so on. However, this is getting messy. So, many programmers prefer operator void*() instead of operator bool().

As you can see, there is a design element to overloading operators. Your decisions about which operators to overload directly influence the ways in which clients can use your classes.

OVERLOADING THE MEMORY ALLOCATION AND DEALLOCATION OPERATORS

C++ gives you the ability to redefine the way memory allocation and deallocation work in your programs. You can provide this customization both on the global level and the class level. This capability is most useful when you are worried about memory fragmentation, which can occur if you allocate and deallocate a lot of small objects. For example, instead of going to the default C++ memory allocation each time you need memory, you could write a memory pool allocator that reuses fixed-size chunks of memory. This section explains the subtleties of the memory allocation and deallocation routines and shows you how to customize them. With these tools, you should be able to write your own allocator if the need ever arises.

How new and delete Really Work

One of the trickiest aspects of C++ is the details of new and delete. Consider this line of code:

SpreadsheetCell* cell = new SpreadsheetCell();

The part “new SpreadsheetCell()” is called the new-expression. It does two things. First, it allocates space for the SpreadsheetCell object by making a call to operator new. Second, it calls the constructor for the object. Only after the constructor has completed does it return the pointer to you.

delete works analogously. Consider this line of code:

delete cell;

This line is called the delete-expression. It first calls the destructor for cell, and then calls operator delete to free the memory.

You can overload operator new and operator delete to control memory allocation and deallocation, but you cannot overload the new-expression or the delete-expression. Thus, you can customize the actual memory allocation and deallocation, but not the calls to the constructor and destructor.

The New-Expression and operator new

There are six different forms of the new-expression, each of which has a corresponding operator new. Earlier chapters in this book already show four new-expressions: new, new[], new(nothrow), and new(nothrow)[]. The following list shows the corresponding four operator new forms from the <new> header file:

void* operator new(size_t size);
void* operator new[](size_t size);
void* operator new(size_t size, const std::nothrow_t&) noexcept;
void* operator new[](size_t size, const std::nothrow_t&) noexcept;

There are two special new-expressions that do no allocation, but invoke the constructor on an existing piece of storage. These are called placement new operators (including both single and array forms). They allow you to construct an object in preexisting memory like this:

void* ptr = allocateMemorySomehow();
SpreadsheetCell* cell = new (ptr) SpreadsheetCell();

This feature is a bit obscure, but it’s important to realize that it exists. It can come in handy if you want to implement memory pools such that you reuse memory without freeing it in between. The corresponding operator new forms look as follows; however, the C++ standard forbids you from overloading them.

void* operator new(size_t size, void* p) noexcept;
void* operator new[](size_t size, void* p) noexcept;

The Delete-Expression and operator delete

There are only two different forms of the delete-expression that you can call: delete, and delete[]; there are no nothrow or placement forms. However, there are all six forms of operator delete. Why the asymmetry? The two nothrow and two placement forms are used only if an exception is thrown from a constructor. In that case, the operator delete is called that matches the operator new that was used to allocate the memory prior to the constructor call. However, if you delete a pointer normally, delete will call either operator delete or operator delete[] (never the nothrow or placement forms). Practically, this doesn’t really matter because the C++ standard says that throwing an exception from delete results in undefined behavior. This means delete should never throw an exception anyway, so the nothrow version of operator delete is superfluous. Also, placement delete should be a no-op, because the memory wasn’t allocated in placement new, so there’s nothing to free. Here are the prototypes for the operator delete forms:

void operator delete(void* ptr) noexcept;
void operator delete[](void* ptr) noexcept;
void operator delete(void* ptr, const std::nothrow_t&) noexcept;
void operator delete[](void* ptr, const std::nothrow_t&) noexcept;
void operator delete(void* p, void*) noexcept;
void operator delete[](void* p, void*) noexcept;

Overloading operator new and operator delete

You can actually replace the global operator new and operator delete routines if you want. These functions are called for every new-expression and delete-expression in the program, unless there are more specific routines in individual classes. However, to quote Bjarne Stroustrup, “... replacing the global operator new and operator delete is not for the fainthearted.” (The C++ Programming Language, third edition, Addison-Wesley, 1997). I don’t recommend it either!

A more useful technique is to overload operator new and operator delete for specific classes. These overloaded operators will be called only when you allocate and deallocate objects of that particular class. Here is an example of a class that overloads the four non-placement forms of operator new and operator delete:

#include <cstddef>
#include <new>

class MemoryDemo
{
    public:
        virtual ~MemoryDemo() = default;

        void* operator new(size_t size);
        void operator delete(void* ptr) noexcept;

        void* operator new[](size_t size);
        void operator delete[](void* ptr) noexcept;

        void* operator new(size_t size, const std::nothrow_t&) noexcept;
        void operator delete(void* ptr, const std::nothrow_t&) noexcept;

        void* operator new[](size_t size, const std::nothrow_t&) noexcept;
        void operator delete[](void* ptr, const std::nothrow_t&) noexcept;
};

Here are implementations of these operators that simply pass the arguments through to calls to the global versions of the operators. Note that nothrow is actually a variable of type nothrow_t:

void* MemoryDemo::operator new(size_t size)
{
    cout << "operator new" << endl;
    return ::operator new(size);
}
void MemoryDemo::operator delete(void* ptr) noexcept
{
    cout << "operator delete" << endl;
    ::operator delete(ptr);
}
void* MemoryDemo::operator new[](size_t size)
{
    cout << "operator new[]" << endl;
    return ::operator new[](size);
}
void MemoryDemo::operator delete[](void* ptr) noexcept
{
    cout << "operator delete[]" << endl;
    ::operator delete[](ptr);
}
void* MemoryDemo::operator new(size_t size, const nothrow_t&) noexcept
{
    cout << "operator new nothrow" << endl;
    return ::operator new(size, nothrow);
}
void MemoryDemo::operator delete(void* ptr, const nothrow_t&) noexcept
{
    cout << "operator delete nothrow" << endl;
    ::operator delete(ptr, nothrow);
}
void* MemoryDemo::operator new[](size_t size, const nothrow_t&) noexcept
{
    cout << "operator new[] nothrow" << endl;
    return ::operator new[](size, nothrow);
}
void MemoryDemo::operator delete[](void* ptr, const nothrow_t&) noexcept
{
    cout << "operator delete[] nothrow" << endl;
    ::operator delete[](ptr, nothrow);
}

Here is some code that allocates and frees objects of this class in several ways:

MemoryDemo* mem = new MemoryDemo();
delete mem;
mem = new MemoryDemo[10];
delete [] mem;
mem = new (nothrow) MemoryDemo();
delete mem;
mem = new (nothrow) MemoryDemo[10];
delete [] mem;

Here is the output from running the program:

operator new
operator delete
operator new[]
operator delete[]
operator new nothrow
operator delete
operator new[] nothrow
operator delete[]

These implementations of operator new and operator delete are obviously trivial and not particularly useful. They are intended only to give you an idea of the syntax in case you ever want to implement nontrivial versions of them.

It might seem overkill to overload all of the various forms of operator new. However, it’s generally a good idea to do so in order to prevent inconsistencies in the memory allocations. If you don’t want to provide implementations for certain forms, you can explicitly delete these using =delete in order to prevent anyone from using them. See the next section for more information.

Explicitly Deleting/Defaulting operator new and operator delete

Chapter 8 shows how you can explicitly delete or default a constructor or assignment operator. Explicitly deleting or defaulting is not limited to constructors and assignment operators. For example, the following class deletes the operator new and new[], which means that this class cannot be dynamically created using new or new[]:

class MyClass
{
    public:
        void* operator new(size_t size) = delete;
        void* operator new[](size_t size) = delete;
};

Using this class in the following ways results in compilation errors:

int main()
{
    MyClass* p1 = new MyClass;
    MyClass* pArray = new MyClass[2];
    return 0;
}

Overloading operator new and operator delete with Extra Parameters

In addition to overloading the standard forms of operator new, you can write your own versions with extra parameters. These extra parameters can be useful for passing various flags or counters to your memory allocation routines. For instance, some runtime libraries use this in debug mode to provide the filename and line number where an object is allocated, so when there is a memory leak, the offending line that did the allocation can be identified.

As an example, here are the prototypes for an additional operator new and operator delete with an extra integer parameter for the MemoryDemo class:

void* operator new(size_t size, int extra);
void operator delete(void* ptr, int extra) noexcept;

The implementation is as follows:

void* MemoryDemo::operator new(size_t size, int extra)
{
    cout << "operator new with extra int: " << extra << endl;
    return ::operator new(size);
}
void MemoryDemo::operator delete(void* ptr, int extra) noexcept
{
    cout << "operator delete with extra int: " << extra << endl;
    return ::operator delete(ptr);
}

When you write an overloaded operator new with extra parameters, the compiler automatically allows the corresponding new-expression. The extra arguments to new are passed with function call syntax (as with nothrow versions). So, you can now write code like this:

MemoryDemo* memp = new(5) MemoryDemo();
delete memp;

The output is as follows:

operator new with extra int: 5
operator delete

When you define an operator new with extra parameters, you should also define the corresponding operator delete with the same extra parameters. You cannot call this operator delete with extra parameters yourself, but it will be called only when you use your operator new with extra parameters and the constructor of your object throws an exception.

Overloading operator delete with Size of Memory as Parameter

An alternate form of operator delete gives you the size of the memory that should be freed as well as the pointer. Simply declare the prototype for operator delete with an extra size parameter.

You can replace operator delete with the version that takes a size for any of the versions of operator delete independently. Here is the MemoryDemo class definition with the first operator delete modified to take the size of the memory to be deleted:

class MemoryDemo
{
    public:
        // Omitted for brevity 
        void* operator new(size_t size);
        void operator delete(void* ptr, size_t size) noexcept;
        // Omitted for brevity
};

The implementation of this operator delete calls the global operator delete without the size parameter because there is no global operator delete that takes the size:

void MemoryDemo::operator delete(void* ptr, size_t size) noexcept
{
    cout << "operator delete with size " << size << endl;
    ::operator delete(ptr);
}

This capability is useful only if you are writing a complicated memory allocation and deallocation scheme for your classes.

SUMMARY

This chapter summarized the rationale for operator overloading and provided examples and explanations for overloading the various categories of operators. Hopefully, this chapter taught you to appreciate the power that it gives you. Throughout this book, operator overloading is used to provide abstractions and easy-to-use class interfaces.

Now it’s time to start delving into the C++ Standard Library. The next chapter starts with an overview of the functionality provided by the C++ Standard Library, followed by chapters that go deeper into specific features of the library.

NOTE

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

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