Overloading C++ Operators - Coding the Professional Way - Professional C++ (2014)

Professional C++ (2014)

Part IIICoding the Professional Way

Chapter 14Overloading C++ Operators

WHAT’S IN THIS CHAPTER?

· Explaining operator overloading

· Rationale for overloading operators

· Limitations, caveats, and choices in operator overloading

· Summary of operators you can, cannot, and should not overload

· How to overload unary plus, unary minus, increment, and decrement

· How to overload the I/O streams operators (operator<< and operator>>)

· How to overload the subscripting (array index) operator

· How to overload the function call operator

· How to overload the dereferencing operators (* and ->)

· How to write conversion operators

· How to overload the memory allocation and deallocation operators

WROX.COM DOWNLOADS FOR THIS CHAPTER

Please note that all the code examples for this chapter are available as a part of this chapter’s code download on the book’s website at www.wrox.com/go/proc++3e on the Download Code tab.

C++ allows you to redefine the meanings of operators, such as +, -, and =, for your classes. Many object-oriented languages do not provide this capability, so you might be tempted to disregard its usefulness in C++. However, it can be beneficial for making your classes behave similarly to built-in types such as ints and doubles. It is even possible to write classes that look like arrays, functions, or pointers.

Chapters 5 and 6 introduce object-oriented design and operator overloading, respectively. Chapters 7 and 8 present the syntax details for objects and for basic operator overloading. This chapter picks up operator overloading where Chapter 8 left off.

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

The second reason to overload operators is to gain greater control over the behavior in your program. For example, you can overload memory allocation and deallocation routines 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 clients 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 “Summary of Overloadable Operators” section 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. There is only one ternary operator: ?:. The main place where this limitation might bother you is when overloading [] (array brackets), 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 (e.g., 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 routines; you can replace the global routines 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 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 whitespace between operator and X. For example, Chapter 8 declares operator+ for SpreadsheetCell objects like this:

friend 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 (usually 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 “Summary of Overloadable Operators” section 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<< andoperator>>, where the left-hand side is the 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 8 mentions 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, but friend functions 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; 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 wouldn’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 vs. reference is easy: you should take every non-primitive parameter type by reference. As Chapters 8 and 10 explain, 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 “Summary of Overloadable Operators” section 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 of the arithmetic. Sometimes the return type is not obvious at first. For example, as Chapter 7 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 “Summary of Overloadable Operators” section.

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 “Summary of Overloadable Operators” section 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 think at first require that you return lvalues, including all of the assignment operators (operator=, operator+=, operator-=, etc.).

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 STL, 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 (if ever) 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 friend functions, summarizes when you should (or should not) overload them, and provides sample prototypes showing the proper return values.

This table should be a useful reference in 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 FRIEND FUNCTION

WHEN TO OVERLOAD

SAMPLE PROTOTYPE

operator+
operator-
operator*
operator/
operator%

Binary arithmetic

Global friend function recommended

Whenever you want to provide these operations for your class

friend T operator+(const T&, const T&);
friend 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 -=

T& operator++();

operator++
operator--

Post-increment and post-decrement

Method recommended

Whenever you overload += and -=

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 friend function recommended

Whenever you want to provide these operations

friend T operator<<(const T&, const T&);
friend 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 friend function recommended

Whenever you want to provide these operations

friend bool operator<(const T&, const T&);
friend bool operator<(const T&, const E&);

operator<<
operator>>

I/O stream operators (insertion and extraction)

Global friend function required

Whenever you want to provide these operations

friend ostream& operator<<
(ostream&, const T&);
friend 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 friend function recommended

Rarely

friend bool operator&&(const T& lhs, const T& rhs);

operator[]

Subscripting (array index) operator

Method required

When you want to support subscripting

E& operator[](int);
const E& operator[](int) const;

operator()

Function call operator

Method required

When you want objects to behave like function pointers

Return type and arguments can vary; see 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 type() 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

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 10 discusses rvalue references, written as && instead of the normal lvalue references, &. They are demonstrated in Chapter 10 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 will modify the argument so it cannot be passed as const. Details are explained in Chapter 10:

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 10. Another example is operator+ to prevent unnecessary memory allocations. The std::string class from the STL, 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 10.

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

using std::rel_ops::operator!=;

using std::rel_ops::operator>;

using std::rel_ops::operator<=;

using std::rel_ops::operator>=;

OVERLOADING THE ARITHMETIC OPERATORS

Chapter 8 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

{

SpreadsheetCell newCell(*this);

newCell.set(-mValue); // call set to update mValue and mString

return newCell;

}

operator- doesn’t change the operand, so this method must construct a new SpreadsheetCell with the negated value, and return a copy of it. Thus, it can’t return a reference.

Overloading Increment and Decrement

There are four ways to add 1 to a variable:

i = i + 1;

i += 1;

++i;

i++;

The last two 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(mValue + 1);

return *this;

}

SpreadsheetCell SpreadsheetCell::operator++(int)

{

SpreadsheetCell oldCell(*this); // Save the current value before incrementing

set(mValue + 1); // Increment

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 “Summary of Overloadable Operators” section 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 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 will read and write strings.

The object on the left of an extraction or insertion operator is the 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 globalfriend functions of the SpreadsheetCell class. The declaration of these functions looks like this:

class SpreadsheetCell

{

public:

// Omitted for brevity

friend std::ostream& operator<<(std::ostream& ostr,

const SpreadsheetCell& cell);

friend std::istream& operator>>(std::istream& istr,

SpreadsheetCell& cell);

// Omitted for brevity

};

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 12 for details. 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. 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.mString;

return ostr;

}

istream& operator>>(istream& istr, SpreadsheetCell& cell)

{

string temp;

istr >> temp;

cell.set(temp);

return istr;

}

The trickiest part of these functions is that, in order for mValue to be set correctly, operator>> must remember to call the set() method on the SpreadsheetCell instead of setting mString directly.

OVERLOADING THE SUBSCRIPTING OPERATOR

Pretend for a few minutes that you have never heard of the vector or array class templates in the STL, 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 as follows:

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. If index x does not exist in the array,

// throws an exception of type out_of_range.

T getElementAt(size_t x) const;

// Sets the value at index x to val. If index x is out of range,

// allocates more space to make it in range.

void setElementAt(size_t x, const T& val);

private:

static const size_t kAllocSize = 4;

void resize(size_t newSize);

// Sets all elements to 0

void initializeElements();

T* mElems;

size_t mSize;

};

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;

mElems = new T[mSize];

initializeElements();

}

template <typename T> Array<T>::~Array()

{

delete [] mElems;

mElems = nullptr;

}

template <typename T> void Array<T>::initializeElements()

{

for (size_t i = 0; i < mSize; i++)

mElems[i] = T();

}

template <typename T> void Array<T>::resize(size_t newSize)

{

// Make a copy of the current elements pointer and size

T* oldElems = mElems;

size_t oldSize = mSize;

// Create new bigger array

mSize = newSize; // store the new size

mElems = new T[newSize]; // Allocate the new array of the new size

initializeElements(); // Initialize all elements to 0

// The new size is always bigger than the old size

for (size_t i = 0; i < oldSize; i++) {

// Copy the elements from the old array to the new one

mElems[i] = oldElems[i];

}

delete [] oldElems; // free the memory for the old array

}

template <typename T> T Array<T>::getElementAt(size_t x) const

{

if (x >= mSize) {

throw std::out_of_range("");

}

return mElems[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);

}

mElems[x] = val;

}

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 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 mElems[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 mElems 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 value or constreference. Ideally, you would provide two operator[]s: one returns a reference and one returns a const reference. You might try to do this as follows:

T& operator[](size_t x);

const T& operator[](size_t x); // Error! Can't overload based on return type.

However, there is one small problem: you can’t overload a method or operator based only on the return type. Thus, the preceding code doesn’t compile. C++ provides a way around this restriction: if you mark the second operator[] with the attribute const, then the compiler can distinguish between the two. If you call operator[] on a const object, it will use the const operator[], and, if you call it on a non-const object, it will use the non-const operator[]. Here are the two operators with the correct prototypes:

T& operator[](size_t x);

const T& operator[](size_t x) 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 mElems[x];

}

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

void printArray(const Array<int>& arr, size_t size);

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, 10);

return 0;

}

void printArray(const Array<int>& arr, size_t size)

{

for (size_t i = 0; i < size; i++) {

cout << arr[i] << " "; // Calls the const operator[] because arr is

// a const object.

}

cout << endl;

}

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.

Non-Integral Array Indices

It is a natural extension of the paradigm of “indexing” into a collection by providing 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 STL associative containers, like std::map, which are described in Chapter 16.

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

template <typename T>

class AssociativeArray

{

public:

AssociativeArray();

virtual ~AssociativeArray();

T& operator[](const std::string& key);

const T& operator[](const std::string& 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++3e.

NOTE You cannot overload the subscripting operator to take more than one parameter. If you want to provide subscripting on more than one index, you can use the function call operator explained in the next section.

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. 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 'margin-bottom:0cm;margin-bottom:.0001pt;line-height: normal;vertical-align:baseline'>class FunctionObject

{

public:

int operator() (int inParam); // function call operator

int doSquare(int inParam); // Normal method

};

// Implementation of overloaded function call operator

int FunctionObject::operator() (int inParam)

{

return doSquare(inParam);

}

// Implementation of normal method

int FunctionObject::doSquare(int inParam)

{

return inParam * inParam;

}

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

An object of a class with a function call operator is called a function object, or functor, for short.

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. You can pass function objects as callback functions to routines that expect function pointers, as long as the function pointer types are templatized. This is discussed in more detail in Chapter 17.

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 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 STL in Chapter 17.

By following the normal method overloading rules, you can write as many operator()s for your classes as you want. Specifically, the various operator()s must have a different number of parameters or different types of parameters. For example, you could add anoperator() to the FunctionObject class that takes a string reference:

int operator() (int inParam);

void operator() (string& str);

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

OVERLOADING THE DEREFERENCING OPERATORS

You can overload three de-referencing operators: *, ->, and ->*. Ignoring ->* for the moment (I’ll come back to it later), consider the built-in meanings of * and ->. * 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 STL uses, discussed in Chapter 16. This chapter teaches you the basic mechanics for overloading the relevant operators in the context of a simple smart pointer class template.

WARNING C++ has two standard smart pointers called std::unique_ptr and std::shared_ptr. It is highly recommended to use these standard smart pointer classes instead of writing your own. The example here is given only to demonstrate how to write dereferencing operators.

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

template <typename T> class Pointer

{

public:

Pointer(T* inPtr);

virtual ~Pointer();

// Prevent assignment and pass by value.

Pointer(const Pointer<T>& src) = delete;

Pointer<T>& operator=(const Pointer<T>& rhs) = delete;

// Dereference operators will go here.

private:

T* mPtr;

};

This smart pointer is about as simple as you can get. All it does is store a dumb 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 real (“dumb”) 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* inPtr) : mPtr(inPtr)

{

}

template <typename T> Pointer<T>::~Pointer()

{

delete mPtr;

mPtr = nullptr;

}

You would like to be able to use the smart pointer 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 will have to provide implementations of operator* and operator-> for this class. These will be implemented in the next two sections.

WARNING You should rarely, if ever, write an implementation of just one of operator* and operator->. You should almost always write both operators together. It would confuse the users of your class if you failed to provide both.

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 to a variable or object from operator*. In the Pointer class, the declaration and definition look 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;

}

As you can see, operator* returns a reference to the object or variable to which the underlying dumb 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 to an object, 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 Is 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 22 discusses the syntactical details in the section called “Pointers to Methods and Members.” You can ignore these details for this example; the only important parts for now are the .* and the ->* operators:

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 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);

string str = cell; // DOES NOT COMPILE!

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

string str = (string)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 string. 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 strings. The prototype looks like this:

operator std::string() const;

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

SpreadsheetCell::operator string() const

{

return mString;

}

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

SpreadsheetCell cell(1.23);

string str = cell; // Works as expected

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

SpreadsheetCell::operator double() const

{

return mValue;

}

Now you can write code like the following:

SpreadsheetCell cell(1.23);

double d1 = cell;

Ambiguity Problems with 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 aSpreadsheetCell 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. However, we don’t want that constructor to be explicit because we generally like the automatic conversion of doubles to SpreadsheetCells, as explained in Chapter 8. Since C++11, you can solve this problem by making the double conversion operator explicit:

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]

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

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 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 ifstatement, will trigger the conversion to the pointer type. The usual pointer type for the conversion operator is void* because that is a pointer type with which you cannot do much except testing it in Boolean expressions.

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?

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 compiler 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. The compiler cannot find an operator!= that takes a Pointer object and a nullptr_t object. You could implement such an operator!= as afriend of the Pointer class:

template <typename T>

bool operator!=(const Pointer<T>& lhs, const std::nullptr_t& rhs)

{

return lhs.mPtr != rhs;

}

However, after implementing this operator!=, the following comparison stops working, because the compiler doesn’t know anymore which operator!= to use.

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

From this example, you might conclude that the operator bool() technique seems only 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 boolpresents 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> smartCell(new SpreadsheetCell);

int i = smartCell; // Converts smartCell Pointer to bool to int.

That’s usually not behavior that you expect or desire. Thus, 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.

WARNING Unless you know a lot about memory allocation strategies, attempts to overload the memory allocation routines are rarely worth the trouble. Don’t overload them just because it sounds like a neat idea. Only do so if you have a genuine requirement and the necessary knowledge.

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, 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[], nothrow new, and nothrow new[]. The following list shows the corresponding four operator new forms from the <new> header file:

void* operator new(size_t size); // For new

void* operator new[](size_t size); // For new[]

void* operator new(size_t size, const nothrow_t&) noexcept; // For nothrow new

void* operator new[](size_t size, const nothrow_t&) noexcept;// For nothrow new[]

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

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: the C++ standard says that throwing an exception from delete results in undefined behavior, which means delete should never throw an exception anyway, so the nothrow version of operator delete is superfluous; and placement delete should be a no-op, because the memory wasn’t allocated in placement operator 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 nothrow_t&) noexcept;

void operator delete[](void* ptr, const 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!

WARNING If you fail to heed my advice and decide to replace the global operator new, keep in mind that you cannot put any code in the operator that makes a call to new because this will cause an infinite loop. For example, you cannot write a message to the console with cout.

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

class MemoryDemo

{

public:

MemoryDemo() {}

virtual ~MemoryDemo() {}

void* operator new(std::size_t size);

void operator delete(void* ptr) noexcept;

void* operator new[](std::size_t size);

void operator delete[](void* ptr) noexcept;

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

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

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

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

};

Here are simple implementations of these operators that 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.

WARNING Whenever you overload operator new, overload the corresponding form of operator delete. Otherwise, memory will be allocated as you specify but freed according to the built-in semantics, which may not be compatible.

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, you can explicitly delete the function using =delete in order to prevent anyone from using it. See the next section.

WARNING Overload all forms of operator new, or explicitly delete forms that you don’t want to get used.

Explicitly Deleting/Defaulting operator new and operator delete

Chapter 7 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 by using new or new[]:

class MyClass

{

public:

void* operator new(std::size_t size) = delete;

void* operator new[](std::size_t size) = delete;

};

Using this class in the following ways will result in compiler 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. For 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(std::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 arg: " << extra << endl;

return ::operator new(size);

}

void MemoryDemo::operator delete(void* ptr, int extra) noexcept

{

cout << "operator delete with extra int arg: " << extra << endl;

return ::operator delete(ptr);

}

When you write an overloaded operator new with extra parameters, the compiler will automatically allow the corresponding new-expression. So, you can now write code like this:

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

delete memp;

The extra arguments to new are passed with function call syntax (as in nothrow new). These extra arguments can be useful for passing various flags or counters to your memory allocation routines. For example, some runtime libraries use this in debug mode to provide the file name 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.

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 youroperator new with extra parameters and the constructor of your object throws an exception.

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.

WARNING If your class declares two identical versions of operator delete except that one takes the size parameter and the other doesn’t, the version without the size parameter will always get called. If you want the version with the size parameter to be used, write only that version.

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(std::size_t size);

void operator delete(void* ptr, std::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" << 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 in on specific features of the library.