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.
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.
Here is a list of things you cannot do when you overload operators:
.
(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.++
, 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.+
for int
s 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.
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.
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:
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.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.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.
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 string
s! 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.
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 bool
s. 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).
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.
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.
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 |
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 string
s 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.
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:
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.
C++ has several unary arithmetic operators. Two of these are unary minus and unary plus. Here is an example of these operators using int
s:
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;
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.
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.
In C++, you use operators not only for arithmetic operations, but also for reading from, and writing to, streams. For example, when you write int
s and string
s 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 SpreadsheetCell
s 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;
}
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.
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.
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
.
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:
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;
.
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.
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.
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.
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->*
.
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 SpreadsheetCell
s to double
s. 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;
Note that writing the
This line now fails to compile. It worked before you wrote The usual pre-C++11 solution to this conundrum is to make the constructor in question
The following code demonstrates its use:
Here is what each line of code means: Sometimes it is useful to be able to use objects in Boolean expressions. For example, programmers often use pointers in conditional statements like this:
Sometimes they write shorthand conditions such as this:
Other times, you see code as follows:
Currently, none of the preceding expressions compile with the
Now the following code compiles and does what you expect:
The output is as follows:
Another alternative is to overload
The following comparisons still work:
However, with
This is correct behavior because
However, after implementing this
From this example, you might conclude that the
That’s usually not behavior that you expect or desire. To prevent such assignments, you could explicitly delete the conversion operators to 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.Solving Ambiguity Problems with Explicit Conversion Operators
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()
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.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 double
s to SpreadsheetCell
s. Since C++11, you can solve this problem by making the double
conversion operator explicit
instead of the constructor:explicit operator double() const;
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]
double
to a SpreadsheetCell
. Because this is in the declaration, this is done by calling the constructor that accepts a double
.operator
string()
conversion operator.operator
double()
conversion operator. Note that because this conversion operator is now declared explicit
, the cast is required.3.3
to a SpreadsheetCell
, followed by operator+
on two SpreadsheetCell
s, followed by a required explicit cast to invoke operator
double()
.Conversions for Boolean Expressions
if (ptr != nullptr) { /* Perform some dereferencing action. */ }
if (ptr) { /* Perform some dereferencing action. */ }
if (!ptr) { /* Do something. */ }
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;
}
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);
}
nullptr
not nullptr
not NULL
not nullptr
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;
}
if (p != NULL) { cout << "not NULL" << endl; }
if (p) { cout << "not nullptr" << endl; }
if (!p) { cout << "nullptr" << endl; }
operator bool()
, the following comparison with nullptr
results in a compilation error:if (p != nullptr) { cout << "not nullptr" << endl; } // Error
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;
}
operator!=
, the following comparison stops working, because the compiler no longer knows which operator!=
to use.if (p != NULL) { cout << "not NULL" << endl; }
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.
int
, long
, long long
, and so on. However, this is getting messy. So, many programmers prefer operator void*()
instead of operator bool()
.
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.
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.
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;
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;
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.
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;
}
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.
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.
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.