Overloading C++ Operators
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 is instrumental 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.
Chapter 5, “Designing with Classes,” and Chapter 6, “Designing for Reuse,” introduce object-oriented design and operator overloading, respectively. Chapter 8, “Gaining Proficiency with Classes and Objects,” and Chapter 9, “Mastering Classes and Objects,” present the syntax details for objects and for basic operator overloading. This chapter picks up operator overloading where Chapter 9 left off.
OVERVIEW OF OPERATOR OVERLOADING
Section titled “OVERVIEW OF OPERATOR OVERLOADING”As Chapter 1, “A Crash Course in C++ and the Standard Library,” explains, operators in C++ are symbols such as +, <, *, and <<. They work on built-in types such as int and double to allow you to perform arithmetic, logical, and other operations. There are also operators such as -> and * that allow you to dereference pointers. The concept of operators in C++ is broad, and even includes [] (array index), () (function call), casting, and the memory allocation and deallocation operators. Operator overloading allows you to change the behavior of language operators for your classes. However, this capability comes with rules, limitations, and choices.
Why Overload Operators?
Section titled “Why Overload Operators?”Before learning how to overload operators, you probably want to know why you would ever want to do so. The reasons vary for the different operators, but the general guiding principle is to make your classes behave like built-in types. The closer your classes are to built-in types, the easier they will be for clients to use. For example, if you want to write a class to represent fractions, it’s quite helpful to have the ability to define what +, -, *, and / mean when applied to objects of that class.
Another reason to overload operators is to gain greater control over the behavior in your program. For example, you can overload memory allocation and deallocation operators for your classes to specify exactly how memory should be distributed and reclaimed for each new object.
It’s important to emphasize that operator overloading doesn’t necessarily make things easier for you as the class developer; its main purpose is to make things easier for users of the class.
Limitations to Operator Overloading
Section titled “Limitations to Operator Overloading”Here is a list of things you cannot do when you overload operators:
- You cannot add new operator symbols. You can only redefine the meanings of operators already in the language. The table in the section “Summary of Overloadable Operators” later in this chapter lists all of the operators that you can overload.
- There are a few operators that you cannot overload, such as
.and.*(member access in an object),::(scope resolution operator), and?:(the conditional operator). 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 change the arity only for the function call, new, and delete operators, and, since C++23, also for the subscripting operator (array index),
[]. 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. - You cannot change the precedence nor the associativity of an operator. The precedence is used to decide which operators need to be executed before other operators, while the associativity can be either left-to-right or right-to-left and specifies in which order operators of the same precedence are executed. Again, this constraint shouldn’t be cause for concern in most programs because there are rarely benefits to changing the order of evaluation, but, in certain domains, it’s something to keep in mind. For example, if you are writing a class to represent mathematical vectors and would like to overload the
^operator to be able to raise a vector to a certain power, then keep in mind that^has lower precedence compared to many other operators such as+. For instance, supposexandyare mathematical vectors, writingx^3+ywill be evaluated asx^(3+y)and not as(x^3)+yas you probably intended. - You cannot redefine operators for built-in types. The operator must be a member function in a class, or at least one of the arguments to a global overloaded operator function must be a user-defined type (for example, a class). This means that you can’t do something ridiculous, such as redefine
+forints to mean subtraction (though you could do so for your own 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, 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. For such dual-meaning operators, you can overload both meanings.
Choices in Operator Overloading
Section titled “Choices in Operator Overloading”When you overload an operator, you write a global function or member function with the name operatorX, where X is the symbol for some operator, and with optional whitespace 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 you write.
Member Function or Global Function
Section titled “Member Function or Global Function”First, you must decide whether your operator should be a member function of your class or a global function. The latter can be a friend of the class, although that should be a last resort — adding friends to a class should be limited as much as possible, as they can access private data members directly and thus circumvent the data-hiding principle.
How do you choose between a member function or a global function? First, you need to understand the difference between these two choices. When the operator is a member function 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 member functions. The C++ language requires some operators to be member functions of a class because they don’t make sense outside of a class. For example,
operator=is tied so closely to the class that it can’t exist anywhere else. The table in the section “Summary of Overloadable Operators” lists those operators that must be member functions. 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 the
<<and>>insertion and extraction streaming operators, where the left-hand side is aniostreamobject, not an object of your class. It also applies to commutative operators like binary+and–, which should allow variables that are not objects of your class on the left-hand side. A global function is required if implicit conversions are desired for the left operand of a binary operator. Chapter 9 discusses this problem. - Operators that can be either member functions or global functions. There is some disagreement in the C++ community on whether it’s better to write member functions or global functions to overload operators. However, I recommend the following rule: make every operator a member function unless you must make it a global function, as described previously. One major advantage to this rule is that member functions can be
virtual, while global functions obviously cannot. Therefore, when you plan to write overloaded operators in an inheritance tree, you should make them member functions if possible.
When you write an overloaded operator as a member function, you should mark it const if it doesn’t change the object. That way, it can be called on const objects.
When you write an overloaded operator as a global function, put it in the same namespace that contains the class for which the operator is written.
Choosing Argument Types
Section titled “Choosing Argument Types”You are somewhat limited in your choice of argument types because, as stated earlier, for most operators you cannot change the number of arguments. For example, operator/ must always have two arguments if it is a global function, and one argument if it’s a member function. The compiler issues an error if it differs from this standard. In this sense, the operator functions are different from normal functions, which you can overload with any number of parameters. Additionally, although you can write the operator for whichever types you want, the choice is usually constrained by the class for which you are writing the operator. For example, if you want to implement addition for class T, you don’t write an operator+ that takes two strings! The real choice arises when you try to determine whether to take parameters by value or by reference and whether to make them const.
The choice of value versus reference is easy: you should take every non-primitive parameter type by reference, unless the function always makes a copy of the passed object, see Chapter 9.
The const decision is also trivial: mark every parameter const unless you actually modify it. The table in the section “Summary of Overloadable Operators” shows sample prototypes for each operator, with the arguments marked const and reference as appropriate.
Choosing Return Types
Section titled “Choosing Return Types”C++ doesn’t determine overload resolution based on return type. Thus, you can specify any return type you want when you write overloaded operators. However, just because you can do something doesn’t mean you should do it. This flexibility implies that you could write confusing code in which comparison operators return pointers, and arithmetic operators return bools. However, you shouldn’t do that. Instead, you should write your overloaded operators such that they return the same types as the operators do for the built-in types. If you write a comparison operator, return a bool. If you write an arithmetic operator, return an object representing the result. Sometimes the return type is not obvious at first. For example, as Chapter 8 mentions, operator= should return a reference to the object on which it’s called in order to support chained 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 (e.g., on the left-hand side of an assignment expression) must be non-const. Otherwise, it should be const. More operators than you might expect require that you return lvalues, including all of the assignment operators (operator=, operator+=, operator-=, and so on).
Choosing Behavior
Section titled “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
Section titled “Operators You Shouldn’t Overload”Some operators should not be overloaded, even though it is permitted. Specifically, the address-of operator (operator&) is not particularly useful to overload and leads to confusion if you do because you are changing fundamental language behavior (taking addresses of variables) in potentially unexpected ways. The entire Standard Library, which uses operator overloading extensively, never overloads the address-of operator.
Additionally, you should avoid overloading the binary Boolean operators operator&& and || because you lose C++‘s short-circuit evaluation rules. Short-circuiting is not possible in that case because all operands need to be evaluated before they can be passed to your overloaded operator function. If your class needs logical operators, provide operator& and | instead.
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. The following snippet demonstrates the comma operator:
int x { 1 };println("{}", (++x, 2 * x)); // Increments x to 2, doubles it, and prints 4.There is rarely a good reason to overload the comma operator.
Summary of Overloadable Operators
Section titled “Summary of Overloadable Operators”The following table lists the operators you can overload, specifies whether they should be member functions of the class or global functions, summarizes when you should (or should not) overload them, and provides sample prototypes showing the proper parameter and return value types. Operators that cannot be overloaded, such as ., .*, ::, and ?: are not in this list.
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 member function.
In this table, T is the name of the class for which the overloaded operator is written, and E is a different type. 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 | MEMBER FUNCTION OR GLOBAL FUNCTION | WHEN TO OVERLOAD | SAMPLE PROTOTYPES |
|---|---|---|---|---|
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 | Member function recommended | Whenever you want to provide these operations for your class | T operator-() const; |
operator++ operator-- | Pre-increment and pre-decrement | Member function recommended | Whenever you overload += and -= taking an arithmetic argument (int, long, …) | T& operator++(); |
operator++ operator-- | Post-increment and post-decrement | Member function recommended | Whenever you overload += and -= taking an arithmetic argument (int, long, …) | T operator++(int); |
operator= | Assignment operator | Member function required | Whenever your class has dynamically allocated resources, or members that are references | T& operator=(const T&); |
operator+= operator-= operator*= operator/= operator%= | Shorthand / compound arithmetic assignment operator | Member function 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 |
operator<<= operator>>= operator&= `operator | = operator^=` | Shorthand / compound bitwise assignment operator | Member function recommended | Whenever you overload the binary bitwise operators and your class is not designed to be immutable |
operator<=> | Three-way comparison operator | Member function recommended | Whenever you want to provide comparison support for your class; if possible, this should be defaulted using =default | auto operator<=>(const T&) const = default; partial_ordering operator<=>(const E&) const; |
operator== | Binary equality operator | Post-C++20: member function recommended Pre-C++20: global function recommended | Whenever you want to provide comparison support for your class, and you cannot default the three-way comparison operator | bool operator==(const T&) const; bool operator==(const E&) const; bool operator==(const T&, const T&); bool operator==(const T&, const E&); |
operator!= | Binary inequality operator | Post-C++20: member function recommended Pre-C++20: global function recommended | Post-C++20: not needed as the compiler automatically provides != when == is supported Pre-C++20: Whenever you want to provide comparison support for your class | bool operator!=(const T&) const; bool operator!=(const E&) const; bool operator!=(const T&, const T&); bool operator!=(const T&, const E&); |
operator< operator> operator<= operator>= | Binary comparison operators | Global function recommended | Whenever you want to provide these operations; not needed when <=> is provided | 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 | |
operator[] | Subscripting (array index) operator | Member function required | When you want to support subscripting | E& operator[](size_t); const E& operator[](size_t) const; |
operator() | Function call operator | Member function required | When you want objects to behave like function pointers | Return type and parameters can vary; see later examples in this chapter |
operator type() | Conversion, or cast, operators (separate operator for each type) | Member function required | When you want to provide conversions from your class to other types | operator double() const; |
operator ""_x | User-defined literal operator | Global function required | When you want to support user-defined literals | T operator""_i(long double d); |
operator new operator new[] | Memory allocation routines | Member function 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 | Member function 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 | Member function recommended for operator* Member function 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
Section titled “Rvalue References”Chapter 9 discusses move semantics and rvalue references. It demonstrates these by defining move assignment operators, which are used by the compiler in cases where the source object is a temporary object that will be destroyed after the assignment, or an object that is explicitly moved from using std::move(). 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&&) noexcept;The preceding table does not include sample prototypes with rvalue references. However, for most operators it can make sense to write both a version using normal lvalue references and a version using rvalue references. Whether or not it does make 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 this operator+ has the following effect depending on the size and the capacity of both operands:
return move(lhs.append(rhs));or
return move(rhs.insert(0, lhs));In fact, string defines several operator+ overloads accepting two strings as arguments and different combinations of lvalue and rvalue references. Here is a list (simplified):
string operator+(const string& lhs, const string& rhs); // No memory reuse.string operator+(string&& lhs, const string& rhs); // Can reuse memory of lhs.string operator+(const string& lhs, string&& rhs); // Can reuse memory of rhs.string operator+(string&& lhs, string&& rhs); // Can reuse memory of lhs or 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.
Precedence and Associativity
Section titled “Precedence and Associativity”In statements containing multiple operators, the precedence of the operators is used to decide which operators need to be executed before other operators. For example, * and / are always executed before + and -.
The associativity can be either left-to-right or right-to-left and specifies in which order operators of the same precedence are executed.
The following table lists the precedence and associativity of all available C++ operators, including those that you cannot overload and operators you haven’t seen mentioned in this book yet. Operators with a lower precedence number are executed before operators with a higher precedence number. In the table, T represents a type, while x, y, and z represent objects:
| PRECEDENCE | OPERATOR | ASSOCIATIVITY |
|---|---|---|
| 1 | :: | Left-to-right |
| 2 | x++ x-- x() x[] T() T{} . -> | Left-to-right |
| 3 | ++x --x +x -x ! ˜ *x &x (T) sizeof co_await new delete new[] delete[] | Right-to-left |
| 4 | .* ->* | Left-to-right |
| 5 | x*y x/y x%y | Left-to-right |
| 6 | x+y x-y | Left-to-right |
| 7 | << >> | Left-to-right |
| 8 | <=> | Left-to-right |
| 9 | < <= > >= | Left-to-right |
| 10 | == != | Left-to-right |
| 11 | x&y | Left-to-right |
| 12 | ^ | Left-to-right |
| 13 | ` | ` |
| 14 | && | Left-to-right |
| 15 | ` | |
| 16 | x?y:z throw co_yield `= += -= *= /= %= <<= >>= &= ^= | =` |
| 17 | , | Left-to-right |
Relational Operators
Section titled “Relational Operators”The following set of function templates for relational operators are defined in <utility> 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. So, if you implement operator== and < for your class, you get the other relational operators for free with these templates.
However, there are a lot of problems with this technique. A first problem is that 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 19, “Function Pointers, Function Objects, and Lambda Expressions”) do not work with those automatically generated relational operators.
Yet another problem with these is that implicit conversions won’t work.
Finally, with C++20’s three-way comparison operator and the fact that C++20 has deprecated the std::rel_ops namespace, there is no longer any reason to still use rel_ops.
Never use std::rel_ops; it has been deprecated since C++20! Instead, to add support for all six comparison operators to a class, just explicitly default or implement operator<=> and possibly operator== for the class. See Chapter 9 for details.
Alternative Notation
Section titled “Alternative Notation”C++ supports the following alternative notations for a selection of operators. These were mainly used in the old days when using character sets that didn’t include certain characters such as ˜, |, and ^.
| OPERATOR | ALTERNATIVE NOTATION | OPERATOR | ALTERNATIVE NOTATION | |
|---|---|---|---|---|
&& | and | != | not_eq | |
&= | and_eq | ` | ||
& | bitand | ` | =` | |
| ` | ` | bitor | ^ | |
˜ | compl | ^= | xor_eq | |
! | not |
OVERLOADING THE ARITHMETIC OPERATORS
Section titled “OVERLOADING THE ARITHMETIC OPERATORS”Chapter 9 shows how to write the binary arithmetic operators and the shorthand arithmetic assignment operators, but it does not cover how to overload the other arithmetic operators.
Overloading Unary Minus and Unary Plus
Section titled “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 minusi = +i; // Unary plusj = +(-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 member function must construct a new SpreadsheetCell with the negated value and return it. Hence, it can’t return a reference. You can use this operator as follows:
SpreadsheetCell c1 { 4 };SpreadsheetCell c3 { -c1 };Overloading Increment and Decrement
Section titled “Overloading Increment and Decrement”There are several ways to add 1 to a variable:
i = i + 1;i = 1 + i;i += 1;++i;i++;The last two forms are called the increment operators. The first of these is prefix increment, which adds 1 to the variable and then returns the newly incremented value for use in the rest of the expression. The second is postfix increment, which also adds 1 to the variable but 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++(); // PrefixSpreadsheetCell operator++(int); // PostfixSpreadsheetCell& operator--(); // PrefixSpreadsheetCell operator--(int); // PostfixThe 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 virtually 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 operators also work on pointers. When you write classes that are smart pointers, for example, you can overload operator++ and operator-- to provide pointer incrementing and decrementing.
OVERLOADING THE BITWISE AND BINARY LOGICAL OPERATORS
Section titled “OVERLOADING THE BITWISE AND BINARY LOGICAL OPERATORS”The bitwise operators are similar to the arithmetic operators, and the bitwise shorthand assignment operators are similar to the arithmetic shorthand assignment operators. However, they are significantly less common, so no examples are shown here. The table in the section “Summary of Overloadable Operators” shows sample prototypes, so you should be able to implement them easily if the need ever arises.
The logical operators are trickier. It’s not recommended to overload && and ||. These operators don’t really apply to individual types: they aggregate results of Boolean expressions. Additionally, when overloading these operators, you lose the short-circuit evaluation, because both the left-hand side and the right-hand side have to be evaluated before they can be bound to the parameters of your overloaded operator && and ||. Thus, it rarely, if ever, makes sense to overload them for specific types.
OVERLOADING THE INSERTION AND EXTRACTION OPERATORS
Section titled “OVERLOADING THE INSERTION AND EXTRACTION OPERATORS”In C++, you use operators not only for arithmetic operations but also for reading from, and writing to, streams. For example, when you write ints and strings to cout, you use the insertion operator <<:
int number { 10 };cout << "The number is " << number << endl;When you read from streams, you use the extraction operator >>:
int number;string str;cin >> number >> str;You can write insertion and extraction operators that work on your classes as well, so that you can read and write them like this:
SpreadsheetCell myCell, anotherCell, aThirdCell;cin >> myCell >> anotherCell >> aThirdCell;cout << myCell << " " << anotherCell << " " << aThirdCell << endl;Before you write the insertion and extraction operators, you need to decide how you want to stream your class out and how you want to read it in. In this example, the SpreadsheetCells simply read and write a single double value.
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 member function 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:
export std::ostream& operator<<(std::ostream& ostr, const SpreadsheetCell& cell);export 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, clog, and more. See Chapter 13, “Demystifying C++ I/O,” for details on streams. Similarly, by making the extraction operator take a reference to an istream, you make it work with any input stream, such as a file input stream, string input stream, 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 the parameter is of type reference-to-const. The extraction operator, however, modifies the SpreadsheetCell object, requiring the parameter to be a reference-to-non-const.
Both operators return a reference to the stream they were given as their first parameter 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;This line is shorthand for:
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 >> for the SpreadsheetCell class:
ostream& operator<<(ostream& ostr, const SpreadsheetCell& cell){ ostr << cell.getValue(); return ostr;}
istream& operator >>(istream& istr, SpreadsheetCell& cell){ double value; istr >> value; cell.set(value); return istr;}OVERLOADING THE SUBSCRIPTING OPERATOR
Section titled “OVERLOADING THE SUBSCRIPTING OPERATOR”Pretend for a few minutes that you have never heard of the vector or array class templates in the Standard Library, and so you have decided to write your own dynamically allocated array class. This class would allow you to set and retrieve elements at specified indices and would take care of all memory allocation “behind the scenes.” A first stab at the class definition for a dynamically allocated array might look like this:
export template <typename T>class Array{ public: // Creates an array with a default size that will grow as needed. Array(); virtual ˜Array();
// Disallow copy constructor and copy assignment. Array& operator=(const Array& rhs) = delete; Array(const Array& src) = delete;
// Move constructor and move assignment operator. Array(Array&& src) noexcept; Array& operator=(Array&& rhs) noexcept;
// 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(std::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(std::size_t x, const T& value);
// Returns the number of elements in the array. std::size_t getSize() const noexcept; private: static constexpr std::size_t AllocSize { 4 }; void resize(std::size_t newSize); T* m_elements { nullptr }; std::size_t m_size { 0 };};The interface supports setting and accessing elements. It provides random-access guarantees: a client could create a default array and set elements 1, 100 and 1000 without worrying about memory management.
Here are the implementations of the member functions:
template <typename T> Array<T>::Array(){ m_elements = new T[AllocSize] {}; // Elements are zero-initialized! m_size = AllocSize;}
template <typename T> Array<T>::˜Array(){ delete[] m_elements; m_elements = nullptr; m_size = 0;}
template <typename T> Array<T>::Array(Array&& src) noexcept : m_elements { std::exchange(src.m_elements, nullptr) } , m_size { std::exchange(src.m_size, 0) }{}
template <typename T> Array<T>& Array<T>::operator=(Array<T>&& rhs) noexcept{ if (this == &rhs) { return *this; } delete[] m_elements; m_elements = std::exchange(rhs.m_elements, nullptr); m_size = std::exchange(rhs.m_size, 0); return *this;}
template <typename T> void Array<T>::resize(std::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 (m_size). for (std::size_t i { 0 }; i < m_size; ++i) { // Copy the elements from the old array to the new one. newArray[i] = m_elements[i]; }
// Delete the old array, and set the new array. delete[] m_elements; m_size = newSize; m_elements = newArray.release();}
template <typename T> const T& Array<T>::getElementAt(std::size_t x) const{ if (x >= m_size) { throw std::out_of_range { "" }; } return m_elements[x];}
template <typename T> void Array<T>::setElementAt(std::size_t x, const T& val){ if (x >= m_size) { // Allocate AllocSize past the element the client wants. resize(x + AllocSize); } m_elements[x] = val;}
template <typename T> std::size_t Array<T>::getSize() const noexcept{ return m_size;}Pay attention to the exception-safe implementation of the resize() member function. First, it creates a new array of appropriate size using make_unique() 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 newly allocated memory automatically. Finally, when both the allocation of the new array and copying all the elements is successful, that is, no exceptions have been thrown, only then do we delete the old m_elements 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 would get destroyed when the destructor for the unique_ptr is called.
To guarantee strong exception safety (see Chapter 14, “Handling Errors”), resize() copies elements from the old array to the newly allocated array. Chapter 26, “Advanced Templates,” discusses and implements a move_assign_if_noexcept() helper function. This helper function can be used in the implementation of resize() so that elements are moved from the old array to the new array, but only if the move assignment operator of the element type is marked as noexcept. If that’s not the case, the elements are copied. With that change, whether elements are moved or copied, strong exception safety remains guaranteed.
Here is a small example of how you could use this class:
Array<int> myArray;for (size_t i { 0 }; i < 20; i += 2) { myArray.setElementAt(i, 100);}for (size_t i { 0 }; i < 20; ++i) { print("{} ", myArray.getElementAt(i));}The output is as follows:
100 0 100 0 100 0 100 0 100 0 100 0 100 0 100 0 100 0 100 0As 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() member functions.
This is where the overloaded subscripting operator comes in. You can add an operator[] to the class as follows:
export template <typename T>class Array{ public: T& operator[](std::size_t x); // Remainder omitted for brevity.};Here is the implementation:
template <typename T> T& Array<T>::operator[](std::size_t x){ if (x >= m_size) { // Allocate AllocSize past the element the client wants. resize(x + AllocSize); } return m_elements[x];}With this change, you can use conventional array index notation like this:
Array<int> myArray;for (size_t i { 0 }; i < 20; i += 2) { myArray[i] = 100;}for (size_t i { 0 }; i < 20; ++i) { print("{} ", myArray[i]);}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 m_elements array.
Providing Read-Only Access with operator[]
Section titled “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 reference-to-const. To provide for this, you need two operator[] overloads: one returning a reference-to-non-const and one returning a reference-to-const:
T& operator[](std::size_t x);const T& operator[](std::size_t x) const;Remember that you can’t overload a member function or operator based only on the return type, so the second overload returns a reference-to-const 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[](std::size_t x) const{ if (x >= m_size) { throw std::out_of_range { "" }; } return m_elements[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) { print("{} ", arr[i]); // Calls the const operator[] because arr is // a const object. } println("");}
int main(){ Array<int> myArray; for (size_t i { 0 }; i < 20; i += 2) { myArray[i] = 100; // Calls the non-const operator[] because // myArray is a non-const object. } printArray(myArray);}Note that the const operator[] is called in printArray() only because the parameter arr is const. If arr were not const, the non-const operator[] would be called, despite 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[](std::size_t x) const{ if (x >= m_size) { static T nullValue { T{} }; return nullValue; } return m_elements[x];}The nullValue static variable is initialized using the zero-initialization 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.
Multidimensional Subscripting Operator
Section titled “ Multidimensional Subscripting Operator”Starting with C++23, a subscripting operator can support multidimensional indexing. The syntax is straightforward. Instead of writing a subscripting operator accepting a single index parameter, you write a subscripting operator with as many index parameters as dimensions you need.
To demonstrate, let’s revisit the Grid class template from Chapter 12, “Writing Generic Code with Templates.” Its interface contains a const and non-const overload of an at(x,y) member function. These at() member functions can be replaced with two-dimensional const and non-const subscripting operators as follows:
template <typename T>class Grid{ public: std::optional<T>& operator[](std::size_t x, std::size_t y); const std::optional<T>& operator[](std::size_t x, std::size_t y) const; // Remainder omitted for brevity.};The syntax simply specifies two parameters, x and y, for these two-dimensional subscripting operators. The implementations are almost identical to the implementations of the original at() member functions:
template <typename T>const std::optional<T>& Grid<T>::operator[](std::size_t x, std::size_t y) const{ verifyCoordinate(x, y); return m_cells[x + y * m_width];}template <typename T>std::optional<T>& Grid<T>::operator[](std::size_t x, std::size_t y){ return const_cast<std::optional<T>&>(std::as_const(*this)[x, y]);}Here is an example of these new operators in action:
Grid<int> myIntGrid { 4, 4 };int counter { 0 };for (size_t y { 0 }; y < myIntGrid.getHeight(); ++y) { for (size_t x { 0 }; x < myIntGrid.getWidth(); ++x) { myIntGrid[x, y] = ++counter; }}for (size_t y { 0 }; y < myIntGrid.getHeight(); ++y) { for (size_t x { 0 }; x < myIntGrid.getWidth(); ++x) { print("{:3} ", myIntGrid[x, y].value_or(0)); } println("");}The output is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16Non-integral Array Indices
Section titled “Non-integral Array Indices”It is a natural extension of the paradigm of “indexing” into a collection to provide a key of some sort; a vector (or in general, any linear array) is a special case where the “key” is just a position in the array. Think of the argument of operator[] as providing a mapping between two domains: the domain of keys and the domain of values. Thus, you can write an operator[] that uses any type as its index. This type does not need to be an integer type. This is done for the Standard Library associative containers, like std::map, which are discussed in Chapter 18, “Standard Library Containers.”
For example, you could create an associative array in which you use string keys instead of integral indices. The operator[] for such a class would accept a string, or better yet string_view, as an argument. Implementing such a class is an exercise for you at the end of this chapter.
static Subscripting Operator
Section titled “ static Subscripting Operator”With C++23, the subscripting operator can be marked as static as long as the implementation of the operator does not require access to this, or, in other words, does not need access to non-static data members and non-static member functions. This allows the compiler to better optimize the code as it doesn’t need to worry about any this pointer. Here is an example where operator[] is marked as static, constexpr (see Chapter 9), and noexcept (Chapter 14):
enum class Figure { Diamond, Heart, Spade, Club };
class FigureEnumToString{ public: static constexpr string_view operator[](Figure figure) noexcept { switch (figure) { case Figure::Diamond: return "Diamond"; case Figure::Heart: return "Heart"; case Figure::Spade: return "Spade"; case Figure::Club: return "Club"; } }};
int main(){ Figure f { Figure::Spade }; FigureEnumToString converter; println("{}", converter[f]); println("{}", FigureEnumToString{}[f]);}OVERLOADING THE FUNCTION CALL OPERATOR
Section titled “OVERLOADING THE FUNCTION CALL OPERATOR”C++ allows you to overload the function call operator, written as operator(). If you write an operator() for your class, you can use objects of that class as if they were function pointers. An object of a class with a function call operator is called a function object, or functor, for short. Here is an example of a simple class with an overloaded operator() and a class member function with the same behavior:
class Squarer{ public: int operator()(int value) const; // Overloaded function call operator. int doSquare(int value) const; // Normal member function.};// Implementation of overloaded function call operator.int Squarer::operator()(int value) const { return doSquare(value); }// Implementation of normal member function.int Squarer::doSquare(int value) const { return value * value; }Here is an example of code that uses the function call operator, contrasted with a call to the normal member function of the class:
int x { 3 };Squarer square;int xSquared { square(x) }; // Call the function call operator.int xSquaredAgain { square.doSquare(xSquared) };// Call the normal member function.println("{} squared is {}, and squared again is {}.", x, xSquared, xSquaredAgain);The output is as follows:
3 squared is 9, and squared again is 81.At first, the function call operator probably seems a little strange. Why would you want to write a special member function for a class to make objects of the class look like function pointers? Why wouldn’t you just write a global function or a standard member function of a class?
The advantage of function objects over standard member functions 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 19.
The advantages of function objects over global functions are more intricate. There are two main benefits:
- Objects can retain information in their data members between repeated calls to their function call operators. For example, a function object might be used to keep a running sum of numbers collected from each call to the function call operator.
- You can customize the behavior of a function object by setting data members. For example, you could write a function object to compare an argument to the function call operator against a data member. This data member could be configurable so that the object could be customized for whatever comparison you want.
Of course, you could implement either of the preceding benefits with global or static variables. However, function objects provide a cleaner way to do it, and besides, using global or static variables should be avoided and can cause problems in a multithreaded application. The true benefits of function objects are demonstrated with the Standard Library in Chapter 20, “Mastering Standard Library Algorithms.”
By following the normal member function overloading rules, you can write as many operator()s for your classes as you want. For example, you could add an operator() to the Squarer class that takes a double:
int operator()(int value) const;double operator()(double value) const;This double overload can be implemented as follows:
double Squarer::operator()(double value) const { return value * value; } static Function Call Operator
Section titled “ static Function Call Operator”Starting with C++23, a function call operator can be marked as static if its implementation does not require access to this, or, in other words, does not need access to non-static data members and non-static member functions. This is similar to how subscripting operators, discussed earlier in this chapter, can be marked as static, and doing so allows the compiler to better optimize the code.
Here is an example, a reduced Squarer functor with a static, constexpr, and noexcept function call operator:
class Squarer{ public: static constexpr int operator()(int value) noexcept { return value * value; }};This functor can be used as follows:
int x { 3 };int xSquared { Squarer::operator()(x) };int xSquaredAgain { Squarer{}(xSquared) };println("{} squared is {}, and squared again is {}.", x, xSquared, xSquaredAgain);Another benefit of static function call operators is that you can easily take their address, for example, &Squarer::operator(), which allows you to use them as if they were function pointers. This can improve the performance when working with the Standard Library algorithms discussed in detail in Chapter 20. Quite a few of those algorithms accept a callable, such as a functor, to customize their behavior. If your functor has a static function call operator, then passing the address of that function call operator to such algorithms allows the compiler to generate more performant code than with a non-static function call operator. The reason is that with a static function call operator, the compiler doesn’t need to worry about any this pointer.
OVERLOADING THE DEREFERENCING OPERATORS
Section titled “OVERLOADING THE DEREFERENCING OPERATORS”You can overload three dereferencing operators: *, ->, and ->*. Ignoring ->* for the moment (I’ll come back to it later), consider the built-in meanings of * and ->. The * operator dereferences a pointer to give you direct access to its value, while -> is shorthand for a * dereference followed by a . member selection. The following code shows the equivalences:
SpreadsheetCell* cell { new SpreadsheetCell };(*cell).set(5); // Dereference plus member selection.cell->set(5); // Shorthand arrow dereference and member selection together.You can overload the dereferencing operators for your classes to make objects of the classes behave like pointers. The main use of this capability is for implementing smart pointers, introduced in Chapter 7, “Memory Management.” It is also useful for iterators, which the Standard Library uses extensively. Iterators are discussed in Chapter 17, “Understanding Iterators and the Ranges Library.” This chapter teaches you the basic mechanics for overloading the relevant operators in the context of a simple smart pointer class template.
C++ has two standard smart pointers called std::unique_ptr and shared_ptr. You should use these standard smart pointers instead of writing your own. The example here is given only to demonstrate how to write dereferencing operators.
Here is an example of a smart pointer class template definition, without the dereferencing operators filled in yet:
export template <typename T> class Pointer{ public: explicit Pointer(T* ptr) : m_ptr { ptr } {} virtual ˜Pointer() { reset(); } // Disallow copy constructor and copy assignment. Pointer(const Pointer& src) = delete; Pointer& operator=(const Pointer& rhs) = delete; // Allow move construction. Pointer(Pointer&& src) noexcept : m_ptr{ std::exchange(src.m_ptr, nullptr)} { } // Allow move assignment. Pointer& operator=(Pointer&& rhs) noexcept { if (this != &rhs) { reset(); m_ptr = std::exchange(rhs.m_ptr, nullptr); } return *this; }
// Dereferencing operators will go here… private: void reset() { delete m_ptr; m_ptr = nullptr; } T* m_ptr { 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.
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.println("{} ", *smartInt);
Pointer<SpreadsheetCell> smartCell { new SpreadsheetCell };smartCell->set(5); // Dereference and member select the set() member function.println("{} ", smartCell->getValue());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.
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*
Section titled “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 member functions with the . operator.
To provide these semantics, you should return a reference from operator*. For the Pointer class this is done as follows:
export template <typename T> class Pointer{ public: // Omitted for brevity T& operator*() { return *m_ptr; } const T& operator*() const { return *m_ptr; } // Omitted for brevity};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 earlier in this chapter, it’s useful to provide both const and non-const overloads of the member function, which return a reference-to-const and a reference-to-non-const, respectively.
Implementing operator–>
Section titled “Implementing operator–>”The arrow operator is a bit trickier. The result of applying the arrow operator should be a member or member function of an object. However, 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 member function selection. Therefore, C++ treats operator-> as a special case. Consider this line:
smartCell->set(5);C++ translates this to the following:
(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:
export template <typename T> class Pointer{ public: // Omitted for brevity T* operator->() { return m_ptr; } const T* operator->() const { return m_ptr; } // Omitted for brevity};You may find it confusing that operator* and operator-> are asymmetric, but once you see them a few times, you’ll get used to it.
What in the World Are operator.* and operator–>*?
Section titled “What in the World Are operator.* and operator–>*?”It’s perfectly legitimate in C++ to take the address of class data members and member functions to obtain pointers to them. However, you can’t access a non-static data member or call a non-static member function without an object. The whole point of class data members and member functions is that they exist on a per-object basis. Thus, when you want to call the member function or access the data member via the pointer, you must dereference the pointer in the context of an object. The syntax details for using operator.* and ->* is deferred until Chapter 19, as it requires knowledge of how to define function pointers.
C++ does not allow you to overload operator.* (just as you can’t overload operator.), but you could overload operator->*. However, it is tricky, and, given that most C++ programmers don’t even know that you can access member functions and data members through pointers, it’s probably not worth the trouble. The shared_ptr smart pointer in the Standard Library, for example, does not overload operator->*.
WRITING CONVERSION OPERATORS
Section titled “WRITING CONVERSION OPERATORS”Going back to the SpreadsheetCell example, consider these two lines of code:
SpreadsheetCell cell { 1.23 };double d1 { cell }; // DOES NOT COMPILE!A SpreadsheetCell contains a double representation, so it seems logical that you could assign it to a double variable. Well, you can’t. The compiler tells you that it doesn’t know how to convert a SpreadsheetCell to a double. You might be tempted to try forcing the compiler to do what you want, like this:
double d1 { (double)cell }; // STILL DOES NOT COMPILE!First, the preceding code still doesn’t compile because the compiler still doesn’t know how to convert the SpreadsheetCell to a double. It already knew from the first line what you wanted it to do, and it would do it if it could. Second, it’s a bad idea in general to add gratuitous casts to your program.
If you want to allow this kind of conversion, you must tell the compiler how to perform it. Specifically, you can write a conversion operator to convert SpreadsheetCells to doubles. The prototype looks like this:
operator double() const;The name of the function is operator double. It has no return type because the return type is specified by the name of the operator: double. It is const because it doesn’t change the object on which it is called. The implementation looks like this:
SpreadsheetCell::operator double() const{ return getValue();}That’s all you need to do to write a conversion operator from SpreadsheetCell to double. Now the compiler accepts the following lines and does the right thing at run time:
SpreadsheetCell cell { 1.23 };double d1 { cell }; // Works as expectedYou can write conversion operators for any type with this same syntax. For example, here is an std::string conversion operator for SpreadsheetCell:
operator std::string() const;And here is an implementation:
SpreadsheetCell::operator std::string() const{ return doubleToString(getValue());}Now you can convert a SpreadsheetCell to a string. However, due to the constructors provided by string, the following does not work:
string str { cell };Instead, you can either use normal assignment syntax instead of uniform initialization, or use an explicit static_cast() as follows:
string str1 = cell;string str2 { static_cast<string>(cell) };Operator auto
Section titled “Operator auto”Instead of explicitly specifying the type that a conversion operator returns, you can specify auto and let the compiler deduce it for you. For example, the double conversion operator of SpreadsheetCell could be written as follows:
operator auto() const { return getValue(); }There is one caveat, the implementation of member functions with auto return type deduction must be visible to users of the class. Hence, this example puts the implementation directly in the class definition.
Also, remember from Chapter 1 that auto strips away reference and const qualifiers. So, if your operator auto returns a reference to a type T, then the deduced type will be T returned by value, resulting in a copy being made. If needed, you can explicitly add reference and const qualifiers; here’s an example:
operator const auto&() const { /* … */ }Solving Ambiguity Problems with Explicit Conversion Operators
Section titled “Solving Ambiguity Problems with Explicit Conversion Operators”Writing the double conversion operator for the SpreadsheetCell object introduces an ambiguity problem. Consider this line:
SpreadsheetCell cell { 6.6 };double d1 { cell + 3.4 }; // 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.4 to a SpreadsheetCell with the double constructor and perform SpreadsheetCell addition. Before you wrote operator double(), the compiler had only one choice: convert 3.4 to a SpreadsheetCell with the double constructor and perform SpreadsheetCell addition. However, now the compiler could do either. It doesn’t want to make a choice you might not like, so it refuses to make any choice at all.
The usual pre-C++11 solution to this conundrum is to make the constructor in question explicit so that the automatic conversion using that constructor is prevented (see Chapter 8). However, you don’t want that constructor to be explicit because you generally like the automatic conversion of doubles to SpreadsheetCells. Since C++11, you can solve this problem by making the double conversion operator explicit instead of the constructor:
explicit operator double() const;With this change the following line compiles fine:
double d1 { cell + 3.4 }; // 10The operator auto as discussed in the previous section can also be marked as explicit.
Conversions for Boolean Expressions
Section titled “Conversions for Boolean Expressions”Sometimes it is useful to be able to use objects in Boolean expressions. For example, programmers often use pointers in conditional statements like this:
if (ptr != nullptr) { /* Perform some dereferencing action. */ }Sometimes they write shorthand conditions such as this:
if (ptr) { /* Perform some dereferencing action. */ }Other times, you see code as follows:
if (!ptr) { /* Do something. */ }Currently, none of the preceding expressions compile with the Pointer smart pointer class template defined earlier. To make them work, we 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:
operator void*() const { return m_ptr; }Now the following code compiles and does what you expect:
void process(const Pointer<SpreadsheetCell>& p){ if (p != nullptr) { println("not nullptr"); } if (p != 0) { println("not 0"); } if (p) { println("not nullptr"); } if (!p) { println("nullptr"); }}
int main(){ Pointer<SpreadsheetCell> smartCell { nullptr }; process(smartCell); println("");
Pointer<SpreadsheetCell> anotherSmartCell { new SpreadsheetCell { 5.0 } }; process(anotherSmartCell);}The output is as follows:
nullptr
not nullptrnot 0not nullptrAnother 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 m_ptr != nullptr; }The following comparisons still work:
if (p != 0) { println("not 0"); }if (p) { println("not nullptr"); }if (!p) { println("nullptr"); }However, with operator bool(), the following comparison with nullptr results in a compilation error:
if (p != nullptr) { println("not nullptr"); } // ErrorThis is because nullptr has its own type called nullptr_t, which is not automatically converted to the integer 0 (false). The compiler cannot find an operator!= that takes a Pointer object and a nullptr_t object. You could implement such an operator!= as a friend of the Pointer class:
export 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};
export template <typename T>bool operator!=(const Pointer<T>& lhs, std::nullptr_t rhs){ return lhs.m_ptr != rhs;}However, after implementing this operator!=, the following comparison stops working, because the compiler no longer knows which operator!= to use:
if (p != 0) { println("not 0"); }From this example, you might conclude that the operator bool() technique only seems appropriate for objects that don’t represent pointers and for which conversion to a pointer type really doesn’t make sense. Unfortunately, even then, adding a conversion operator to bool presents some other unanticipated consequences. C++ applies promotion rules to silently convert bool to int whenever the opportunity arises. Therefore, with the operator bool(), the following code compiles and runs:
Pointer<SpreadsheetCell> anotherSmartCell { new SpreadsheetCell { 5.0 } };int i { anotherSmartCell }; // Converts Pointer to bool to int.That’s usually not behavior that you expect or desire. To prevent such assignments, you could explicitly delete the conversion operators to int, long, long long, and so on. However, this is getting messy. So, many programmers prefer operator void*() instead of operator bool().
As you can see, there is a design element to overloading operators. Your decisions about which operators to overload directly influence the ways in which clients can use your classes.
OVERLOADING THE MEMORY ALLOCATION AND DEALLOCATION OPERATORS
Section titled “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.
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
Section titled “How new and delete Really Work”One of the trickiest aspects of C++ is the details of new and delete. Consider this line of code:
SpreadsheetCell* cell { new SpreadsheetCell {} };The part new SpreadsheetCell{} is called the new-expression. It does two things. First, it allocates space for the SpreadsheetCell object by making a call to operator new. Second, it calls the constructor for the object. Only after the constructor has completed does it return the pointer to you.
delete works analogously. Consider this line of code:
delete cell;This line is called the delete-expression. It first calls the destructor for cell and then calls operator delete to free the memory.
You can overload operator new and operator delete to control memory allocation and deallocation, but you cannot overload the new-expression or the delete-expression. Thus, you can customize the actual memory allocation and deallocation, but not the calls to the constructor and destructor.
The New-Expression and operator new
Section titled “The New-Expression and operator new”There are six different forms of the new-expression, each of which has a corresponding operator new. Earlier chapters in this book already show four new-expressions: new, new[], new(nothrow), and new(nothrow)[]. The following list shows the corresponding four operator new overloads defined in <new>:
void* operator new(std::size_t size);void* operator new[](std::size_t size);void* operator new(std::size_t size, const std::nothrow_t&) noexcept;void* operator new[](std::size_t size, const std::nothrow_t&) noexcept;There are two special new-expressions that don’t do any allocation but invoke the constructor on an already allocated piece of memory. These are called placement new operators (including both single and array forms). They allow you to construct an object in pre-allocated memory like this:
void* ptr { allocateMemorySomehow() };SpreadsheetCell* cell { new (ptr) SpreadsheetCell {} };The two corresponding operator new overloads for these look as follows; however, the C++ standard forbids you to overload them:
void* operator new(std::size_t size, void* p) noexcept;void* operator new[](std::size_t size, void* p) noexcept;This feature is a bit obscure, but it’s important to know that it exists. It can come in handy if you want to implement memory pools by reusing memory without freeing it in between. This allows you to construct and destruct instances of an object without re-allocating memory for each new instance. Chapter 29, “Writing Efficient C++,” gives an example of a memory pool implementation.
The Delete-Expression and operator delete
Section titled “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 overloads of operator delete. Why the asymmetry? The two nothrow and two placement overloads 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 calls operator delete and delete[] calls 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 (for example, from a destructor called by delete) results in undefined behavior. This means delete should never throw an exception anyway, so the nothrow overload of operator delete is superfluous. Also, placement delete should be a no-op, because placement new doesn’t allocate any memory, so there’s nothing to free.
Here are the prototypes for the six operator delete overloads corresponding to the six operator new overloads:
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* ptr, void*) noexcept;void operator delete[](void* ptr, void*) noexcept;Overloading operator new and operator delete
Section titled “Overloading operator new and operator delete”You can 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!
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 recursion. For example, you cannot write a message to the console with print().
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:
export class MemoryDemo{ public: virtual ˜MemoryDemo() = default;
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 implementations of these operators that simply write out a message to the standard output and 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){ println("operator new"); return ::operator new(size);}void MemoryDemo::operator delete(void* ptr) noexcept{ println("operator delete"); ::operator delete(ptr);}void* MemoryDemo::operator new[](size_t size){ println("operator new[]"); return ::operator new[](size);}void MemoryDemo::operator delete[](void* ptr) noexcept{ println("operator delete[]"); ::operator delete[](ptr);}void* MemoryDemo::operator new(size_t size, const nothrow_t&) noexcept{ println("operator new nothrow"); return ::operator new(size, nothrow);}void MemoryDemo::operator delete(void* ptr, const nothrow_t&) noexcept{ println("operator delete nothrow"); ::operator delete(ptr, nothrow);}void* MemoryDemo::operator new[](size_t size, const nothrow_t&) noexcept{ println("operator new[] nothrow"); return ::operator new[](size, nothrow);}void MemoryDemo::operator delete[](void* ptr, const nothrow_t&) noexcept{ println("operator delete[] nothrow"); ::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 newoperator deleteoperator new[]operator delete[]operator new nothrowoperator deleteoperator new[] nothrowoperator 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.
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 the various forms of operator new and operator delete. However, it’s generally a good idea to do so to prevent inconsistencies in memory allocations. If you don’t want to provide implementations for certain overloads, you can explicitly delete these using =delete to prevent anyone from using them. See the next section for more information.
Overload all forms of operator new and operator delete, or explicitly delete overloads that you don’t want to get used, to prevent inconsistencies in the memory allocations.
Explicitly Deleting or Defaulting operator new and operator delete
Section titled “Explicitly Deleting or Defaulting operator new and operator delete”Chapter 8 shows how you can explicitly delete or default a constructor or assignment operator. Explicitly deleting or defaulting is not limited to constructors and assignment operators. For example, the following class deletes operator new and new[], which means that objects of this class cannot be dynamically allocated using new or new[]:
class MyClass{ public: void* operator new(std::size_t) = delete; void* operator new[](std::size_t) = delete; void* operator new(std::size_t, const std::nothrow_t&) noexcept = delete; void* operator new[](std::size_t, const std::nothrow_t&) noexcept = delete;};Using this class as follows results in compilation errors:
MyClass* p1 { new MyClass };MyClass* p2 { new MyClass[2] };MyClass* p3 { new (std::nothrow) MyClass };Overloading operator new and operator delete with Extra Parameters
Section titled “Overloading operator new and operator delete with Extra Parameters”In addition to overloading the standard forms of operator new, you can write your own versions with extra parameters. These extra parameters can be useful for passing various flags or counters to your memory allocation routines. For instance, some runtime libraries use this in debug mode to provide the filename and line number where an object is allocated, so when there is a memory leak, the offending line that did the allocation can be identified.
As an example, here are the prototypes for an additional operator new and operator delete with an extra integer parameter for the MemoryDemo class:
void* operator new(std::size_t size, int extra);void operator delete(void* ptr, int extra) noexcept;The implementations are as follows:
void* MemoryDemo::operator new(std::size_t size, int extra){ println("operator new with extra int: {}", extra); return ::operator new(size);}void MemoryDemo::operator delete(void* ptr, int extra) noexcept{ println("operator delete with extra int: {}", extra); 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 overloads). 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: 5operator deleteWhen you define an operator new with extra parameters, you should also define the corresponding operator delete with the same extra parameters. However, you cannot call this operator delete with extra parameters yourself; it will be called only when you use your operator new with extra parameters and the constructor of your object throws an exception.
Overloading operator delete with Size of Memory as Parameter
Section titled “Overloading operator delete with Size of Memory as Parameter”An alternate form of operator delete gives you the size of the memory that should be freed as well as the pointer. Simply declare the prototype for operator delete with an extra size parameter.
If a class declares two overloads of operator delete with one overload taking the size as a parameter and the other doesn’t, the overload without the size parameter will always get called. If you want the overload with the size parameter to be used, write only that overload.
You can replace operator delete with an overload that takes a size for any of the overloads 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:
export 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 again simply calls the global operator delete:
void MemoryDemo::operator delete(void* ptr, size_t size) noexcept{ println("operator delete with size {}", size); ::operator delete(ptr, size);}This capability is useful only if you are writing a complicated memory allocation and deallocation scheme for your classes.
OVERLOADING USER-DEFINED LITERAL OPERATORS
Section titled “OVERLOADING USER-DEFINED LITERAL OPERATORS”C++ has a number of built-in literal types that you can use in your code. Here are some examples:
'a': Character"A string": Zero-terminated sequence of characters, C-style string3.14f:floatsingle-precision floating-point value0xabc: Hexadecimal value
C++ also allows you to define your own literals, and the Standard Library does exactly that; it provides a number of additional literal types to construct Standard Library objects. Let’s take a look at these first and then see how you can define your own.
Standard Library Literals
Section titled “Standard Library Literals”The C++ Standard Library defines the following standard literals. Note that these literals do not start with an underscore:
| LITERAL | CREATES INSTANCES OF … | EXAMPLE | REQUIRES NAMESPACE |
|---|---|---|---|
s | string | auto myString { "Hello"s }; | string_literals |
sv | string_view | auto myStringView { "Hello"sv }; | string_view_literals |
h, min, s, ms, us, ns | chrono::duration1 | auto myDuration { 42min }; | chrono_literals |
y, d | chrono::year and day1 | auto thisYear { 2024y }; | chrono_literals |
i, il, if | complex<T> with T equal to double, long double, float, respectively | auto myComplexNumber { 1.3i }; | complex_literals |
[^1] Discussed in Chapter 22, “Date and Time Utilities.”
Technically, all of these are defined in subnamespaces of std::literals, for example std::literals::string_literals. However, both string_literals and literals are inline namespaces that automatically make their contents available in their parent namespace. Hence, if you want to use s string literals, you can use any of the following using directives:
using namespace std;using namespace std::literals;using namespace std::string_literals;using namespace std::literals::string_literals;User-Defined Literals
Section titled “User-Defined Literals”User-defined literals should start with exactly one underscore. Some examples are _i, _s, _km, _miles, _K, and so on.
User-defined literals are implemented by writing literal operators. A literal operator can work in raw or cooked mode. In raw mode, your literal operator receives a sequence of characters, while in cooked mode your literal operator receives a specific interpreted type. For example, take the C++ literal 123. A raw literal operator receives this as a sequence of characters '1', '2', '3'. A cooked literal operator receives this as the integer 123. The literal 0x23 is received by a raw operator as the characters '0', 'x', '2', '3', while a cooked operator receives the integer 35. A literal such as 3.14 is received by a raw operator as '3', '.', '1', '4', while a cooked operator receives the floating-point value 3.14.
Cooked-Mode Literal Operator
Section titled “Cooked-Mode Literal Operator”A cooked-mode literal operator should have either of the following:
- To process numeric values: One parameter of type
unsigned long long,long double,char,wchar_t,char8_t,char16_t, orchar32_t - To process strings: Two parameters where the first is a C-style string and the second is the length of the string, for instance,
(const char* str,std::size_t len)
For example, the following code defines a Length class storing a length in meters. The constructor is private because users should only be able to construct a Length instance using the provided user-defined literals. The code provides cooked literal operators for user-defined literal operators _km and _m. Both of these are friends of Length so that they can access the private constructor. There must not be any space between the "" and the underscore of these operators.
// A class representing a length. The length is always stored in meters.class Length{ public: long double getMeters() const { return m_length; } // The user-defined literals _km and _m are friends of Length so they // can use the private constructor. friend Length operator ""_km(long double d); friend Length operator ""_m(long double d); private: // Private constructor because users should only be able to construct a // Length using the provided user-defined literals. Length(long double length) : m_length { length } {} long double m_length;};Length operator ""_km(long double d) // Cooked _km literal operator{ return Length { d * 1000 }; // Convert to meters.}Length operator ""_m(long double d) // Cooked _m literal operator{ return Length { d };}These literal operators can be used as follows:
Length d1 { 1.2_km };auto d2 { 1.2_m };println("d1 = {}m; d2 = {}m", d1.getMeters(), d2.getMeters());Here is the output:
d1 = 1200m; d2 = 1.2mTo demonstrate the variant of a cooked literal operator accepting a const char* and a size_t, we can re-create the standard string literal, s, provided by the Standard Library, to construct an std::string. Let’s call the literal _s.
string operator ""_s(const char* str, size_t len){ return string { str, len };}This literal operator can be used as follows:
string str1 { "Hello World"_s };auto str2 { "Hello World"_s }; // str2 has as type stringWithout the _s literal operator, the auto type deduction would result in const char*:
auto str3 { "Hello World" }; // str3 has as type const char*Raw-Mode Literal Operator
Section titled “Raw-Mode Literal Operator”A raw-mode literal operator requires one parameter of type const char*, a zero-terminated C-style string. The following example defines the earlier literal operator _m as a raw literal operator:
Length operator ""_m(const char* str){ // Implementation omitted; it requires parsing the C-style string // converting it to a long double, and constructing a Length. …}Using this raw-mode literal operator is the same as using the cooked version.
SUMMARY
Section titled “SUMMARY”This chapter summarized the rationale for operator overloading and provided examples and explanations for overloading the various categories of operators. Ideally, 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.
EXERCISES
Section titled “EXERCISES”By solving the following exercises, you can practice the material discussed in this chapter. Solutions to all exercises are available with the code download on the book’s website at www.wiley.com/go/proc++6e. However, if you are stuck on an exercise, first reread parts of this chapter to try to find an answer yourself before looking at the solution from the website.
- Exercise 15-1: Implement an
AssociativeArrayclass template. The class should store a number of elements in avector, where each element consists of a key and a value. The key is always astring, while the type of the value can be specified using a template type parameter. Provide overloaded subscripting operators so that elements can be retrieved based on their key. Test your implementation in yourmain()function. Note: this exercise is just to practice implementing subscripting operators using non-integral indices. In practice, you should just use thestd::mapclass template provided by the Standard Library and discussed in Chapter 18 for such an associative array. - Exercise 15-2: Take your
Personclass implementation from Exercise 13-2 and add implementations of the insertion and extraction operators to it. Make sure that your extraction operator can read back what your insertion operator writes out. - Exercise 15-3: Add a
stringconversion operator to your solution of Exercise 15-2. The operator simply returns astringconstructed from the first and last name of the person. - Exercise 15-4: Start from your solution of Exercise 15-3 and add a user-defined literal operator
_pthat constructs aPersonfrom a string literal. It should support spaces in last names, but not in first names. For example,"Peter Van Weert"_pshould result in aPersonobject with first name Peter and last name Van Weert.