Common Operators to Overload
Most of the work in overloading operators is boilerplate code. That is little wonder, since operators are merely syntactic sugar. Their actual work could be done by (and often is forwarded to) plain functions. But it is important that you get this boilerplate code right. If you fail, either your operator’s code won’t compile, your users’ code won’t compile, or your users’ code will behave surprisingly.
Assignment Operator
There's a lot to be said about assignment. However, most of it has already been said in GMan's famous Copy-And-Swap FAQ, so I'll skip most of it here, only listing the perfect assignment operator for reference:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
Stream Insertion and Extraction
Disclaimer |
For overloading << and >> as bitwise shift operators, skip to the section Binary Arithmetic Operators. |
The bitwise shift operators <<
and >>
, although still used in hardware interfacing for the bit-manipulation functions they inherit from C, have become more prevalent as overloaded stream input and output operators in most applications.
The stream operators, among the most commonly overloaded operators, are binary infix operators for which the syntax does not specify any restriction on whether they should be members or non-members.
However, their left operands are streams from the standard library, and you cannot add member functions to those1, so you need to implement these operators for your own types as non-member functions2.
The canonical forms of the two are these:
std::ostream& operator<<(std::ostream& os, const T& obj)
{
// Write obj to stream
return os;
}
std::istream& operator>>(std::istream& is, T& obj)
{
// Read obj from stream
if( /* no valid object of T found in stream */ )
is.setstate(std::ios::failbit);
return is;
}
When implementing operator>>
, manually setting the stream’s state is only necessary when the reading itself succeeded, but the result is not what would be expected.
1 Note that some of the <<
overloads of the standard library are implemented as member functions, and some as free functions. Only the locale-dependent functions are member functions, such as operator<<(long)
.
2 According to the rules of thumb, the insertion/extraction operators should be member functions because they modify the left operand. However, we cannot follow the rules of thumb here.
Function Call Operator
The function call operator, used to create function objects, also known as functors, must be defined as a member function, so it always has the implicit this
argument of member functions. Other than this, it can be overloaded to take any number of additional arguments, including zero.
Here's an example of the syntax:
struct X {
// Overloaded call operator
int operator()(const std::string& y) {
return /* ... */;
}
};
Usage:
X f;
int a = f("hello");
Throughout the C++ standard library, function objects are always copied. Your own function objects should therefore be cheap to copy. If a function object absolutely needs to use data which is expensive to copy, it is better to store that data elsewhere and have the function object refer to it.
Comparison Operators
This section has been moved elsewhere |
See this FAQ answer for overloading the binary infix == , != , < , > , <= , and >= operators, as well as the <=> three-way comparison, aka. "spaceship operator" in C++20. There is so much to say about comparison operators that it would exceed the scope of this answer. |
In the most simple case, you can overload all comparison comparison operators by defaulting <=>
in C++20:
#include <compare>
struct X {
// defines ==, !=, <, >, <=, >=, <=>
friend auto operator<=>(const X&, const X&) = default;
};
If you can't do this, continue to the linked answer.
Logical Operators
The unary prefix negation !
should be implemented as a member function. It is usually not a good idea to overload it because of how rare and surprising it is.
struct X {
X operator!() const { return /* ... */; }
};
The remaining binary logical operators (||
, &&
) should be implemented as free functions. However, it is very unlikely that you would find a reasonable use case for these1.
X operator&&(const X& lhs, const X& rhs) { return /* ... */; }
X operator||(const X& lhs, const X& rhs) { return /* ... */; }
1 It should be noted that the built-in version of ||
and &&
use shortcut semantics. While the user defined ones (because they are syntactic sugar for method calls) do not use shortcut semantics. User will expect these operators to have shortcut semantics, and their code may depend on it, Therefore it is highly advised NEVER to define them.
Arithmetic Operators
Unary Arithmetic Operators
The unary increment and decrement operators come in both prefix and postfix flavor. To tell one from the other, the postfix variants take an additional dummy int argument. If you overload increment or decrement, be sure to always implement both prefix and postfix versions.
Here is the canonical implementation of increment, decrement follows the same rules:
struct X {
X& operator++()
{
// Do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
Note that the postfix variant is implemented in terms of prefix. Also note that postfix does an extra copy.1
Overloading unary minus and plus is not very common and probably best avoided. If needed, they should probably be overloaded as member functions.
1 Also note that the postfix variant does more work and is therefore less efficient to use than the prefix variant. This is a good reason to generally prefer prefix increment over postfix increment. While compilers can usually optimize away the additional work of postfix increment for built-in types, they might not be able to do the same for user-defined types (which could be something as innocently looking as a list iterator). Once you got used to do i++
, it becomes very hard to remember to do ++i
instead when i
is not of a built-in type (plus you'd have to change code when changing a type), so it is better to make a habit of always using prefix increment, unless postfix is explicitly needed.
Binary Arithmetic Operators
For the binary arithmetic operators, do not forget to obey the third basic rule operator overloading: If you provide +
, also provide +=
, if you provide -
, do not omit -=
, etc. Andrew Koenig is said to have been the first to observe that the compound assignment operators can be used as a base for their non-compound counterparts. That is, operator +
is implemented in terms of +=
, -
is implemented in terms of -=
, etc.
According to our rules of thumb, +
and its companions should be non-members, while their compound assignment counterparts (+=
, etc.), changing their left argument, should be a member. Here is the exemplary code for +=
and +
; the other binary arithmetic operators should be implemented in the same way:
struct X {
X& operator+=(const X& rhs)
{
// actual addition of rhs to *this
return *this;
}
};
inline X operator+(const X& lhs, const X& rhs)
{
X result = lhs;
result += rhs;
return result;
}
operator+=
returns its result per reference, while operator+
returns a copy of its result. Of course, returning a reference is usually more efficient than returning a copy, but in the case of operator+
, there is no way around the copying. When you write a + b
, you expect the result to be a new value, which is why operator+
has to return a new value.1
Also note that operator+
can be slightly shortened by passing lhs
by value, not by reference.
However, this would be leaking implementation details, make the function signature asymmetric, and would prevent named return value optimization where result
is the same object as the one being returned.
Sometimes, it's impractical to implement @
in terms of @=
, such as for matrix multiplication.
In that case, you can also delegate @=
to @
:
struct Matrix {
// You can also define non-member functions inside the class, i.e. "hidden friends"
friend Matrix operator*(const Matrix& lhs, const Matrix& rhs) {
Matrix result;
// Do matrix multiplication
return result;
}
Matrix& operator*=(const Matrix& rhs)
{
return *this = *this * rhs; // Assuming operator= returns a reference
}
};
The bit manipulation operators ~
&
|
^
<<
>>
should be implemented in the same way as the arithmetic operators. However, (except for overloading <<
and >>
for output and input) there are very few reasonable use cases for overloading these.
1 Again, the lesson to be taken from this is that a += b
is, in general, more efficient than a + b
and should be preferred if possible.
Subscript Operator
The subscript operator is a binary operator which must be implemented as a class member. It is used for container-like types that allow access to their data elements by a key.
The canonical form of providing these is this:
struct X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
Unless you do not want users of your class to be able to change data elements returned by operator[]
(in which case you can omit the non-const variant), you should always provide both variants of the operator.
Operators for Pointer-like Types
For defining your own iterators or smart pointers, you have to overload the unary prefix dereference operator *
and the binary infix pointer member access operator ->
:
struct my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
Note that these, too, will almost always need both a const and a non-const version.
For the ->
operator, if value_type
is of class
(or struct
or union
) type, another operator->()
is called recursively, until an operator->()
returns a value of non-class type.
The unary address-of operator should never be overloaded.
For operator->*()
(and more details about operator->
) see this question. operator->*()
is rarely used and thus rarely ever overloaded. In fact, even iterators do not overload it.
Continue to Conversion Operators.