Which operators you can overload in C++ is a fixed list, and the rules about them are strict. Overloading lets your own types behave like the built-in ones, so a [[Money]] class can be added, compared and printed with the operators you already know. The technique is powerful and easy to abuse, and the discipline that keeps it sane is a simple rule: overload an operator only when its meaning for your type is obvious.
The operators you may overload
C++ allows you to overload most of the operators. The ones you cannot are the ones the language needs for itself.
- Cannot be overloaded —
.,::,.*,->,sizeof,typeid,alignof, and the conditional operator?:. - Cannot be overloaded — the
=assignment operator must be a non-static member function. - Cannot be overloaded — the
()call operator must be a member function. - Cannot be overloaded —
[,]] subscript,->, and the conversion operators must be members. - Cannot be overloaded —
&and*are members if you overloadoperator neworoperator delete. - Cannot be overloaded —
++and--must be members. - Cannot be overloaded — comparison operators in C++20, because of the spaceship relation. In C++17 you can overload them freely.
Everything else — the arithmetic operators, the comparison operators, the stream operators, operator[], operator() — is available.
A first overload: adding two values
An operator overload is a function with a special name. The parameters decide which types take part.
#include
class Duration {
public:
explicit Duration(int minutes) : minutes_(minutes) {}
int minutes() const { return minutes_; }
private:
int minutes_;
};
Duration operator+(const Duration& left, const Duration& right) {
return Duration(left.minutes() + right.minutes());
}
std::ostream& operator<<(std::ostream& stream, const Duration& value) {
stream << value.minutes() << " min";
return stream;
}
int main() {
Duration first(45);
Duration second(30);
Duration total = first + second;
std::cout << "first: " << first << '\n';
std::cout << "total: " << total << '\n';
return 0;
}The output shows the custom type printing itself with the stream operator:
first: 45 min
total: 75 minNotice that operator<< returns the stream by reference. That is what allows the calls to be chained, exactly like the built-in operators.
Member or free function
An overload can be a member function or a free function, and the choice is mostly about which arguments should be implicitly converted.
- A member overload has access to the object's private members and implicitly takes the left-hand operand as
this. - A free overload treats both operands equally and is the right choice for symmetric operations.
- A member overload with a
constafter the parameter list is a read-only operation.
The rule of thumb: if the operation is symmetric, such as addition, write it as a free function. If it modifies the left-hand operand, such as +=, write it as a member.
#include
class Duration {
public:
explicit Duration(int minutes) : minutes_(minutes) {}
int minutes() const { return minutes_; }
Duration& operator+=(const Duration& other) {
minutes_ += other.minutes();
return *this;
}
private:
int minutes_;
};
Duration operator+(const Duration& left, const Duration& right) {
Duration result(left.minutes() + right.minutes());
return result;
}
int main() {
Duration first(45);
Duration second(30);
first += second;
std::cout << "after +=: " << first.minutes() << " minutes\n";
Duration third = first + second;
std::cout << "after +: " << third.minutes() << " minutes\n";
return 0;
}The output shows the member operator modifying in place and the free operator producing a new value:
after +=: 75 minutes
after +: 105 minutesOverloading the stream operators
operator<< and operator>> are the most commonly overloaded operators, because they are how a type is printed and read. The pattern is fixed.
- The left-hand parameter is the stream, taken by reference.
- The right-hand parameter is your type, usually taken by
constreference for output. - The return type is the stream reference.
#include
#include
#include
class Coordinates {
public:
Coordinates(int x, int y) : x_(x), y_(y) {}
int x() const { return x_; }
int y() const { return y_; }
private:
int x_;
int y_;
};
std::ostream& operator<<(std::ostream& stream, const Coordinates& point) {
stream << '(' << point.x() << ", " << point.y() << ')';
return stream;
}
std::istream& operator>>(std::istream& stream, Coordinates& point) {
int x = 0;
int y = 0;
stream >> x >> y;
if (stream) {
point = Coordinates(x, y);
}
return stream;
}
int main() {
Coordinates point(12, 34);
std::cout << "point: " << point << '\n';
Coordinates read_in(0, 0);
std::istringstream input("56 78");
input >> read_in;
std::cout << "read: " << read_in << '\n';
return 0;
}The output shows both directions working:
point: (12, 34)
read: (56, 78)The rules that keep overloads sane
- Do not overload operators whose meaning is unclear. What would
operator==mean for your type? If the answer is not obvious, do not define it. - Keep the parameters and the return type consistent with the built-in operator.
a + bshould return something that can be added to anothera. - Do not break the symmetry of the built-in behaviour. If
a == bis true thenb == amust be true. - Do not overload
&&,||or,. They lose their short-circuit and sequencing behaviour, which surprises everyone. - **Return
*thisby reference from compound assignment operators** so they can be chained. - Mark read-only member overloads
constso they can be called on const objects.
A worked example with comparison
Overloading the comparison operators lets your type work with the standard algorithms and with std::sort.
#include
#include
#include
class Reading {
public:
Reading(const std::string& label, int value) : label_(label), value_(value) {}
const std::string& label() const { return label_; }
int value() const { return value_; }
private:
std::string label_;
int value_;
};
bool operator<(const Reading& left, const Reading& right) {
return left.value() < right.value();
}
std::ostream& operator<<(std::ostream& stream, const Reading& reading) {
stream << reading.label() << '=' << reading.value();
return stream;
}
int main() {
std::vector readings = {
Reading{"oil", 42}, Reading{"coolant", 91}, Reading{"battery", 12}
};
std::sort(readings.begin(), readings.end());
for (const Reading& reading : readings) {
std::cout << reading << ' ';
}
std::cout << '\n';
return 0;
}The output shows the readings sorted by value:
battery=12 oil=42 coolant=91Because operator< is defined, std::sort works on the vector without knowing anything about the type.
Quick reference
- Most operators can be overloaded;
.,::,sizeofandtypeidcannot. - Assignment, call, subscript, arrow, increment and decrement must be member functions.
- Write symmetric operations as free functions and modifying operations as members.
operator<<takes the stream by reference and returns it, so calls chain.- Never overload
&&,||or,. - Keep the semantics of the built-in operator or you will confuse every reader.
Try it yourself: write a [[Money]] class holding an amount in pence, then overload [[+]], [[+=]], [[-]], [[operator<<]] and [[operator<]]. Print a list of amounts sorted from smallest to largest. Finally, try to add a [[Money]] to a plain [[int]] and see what the compiler says about the missing conversion, then add an explicit conversion operator and try again.