Operator Overloading
When you use std::string, you can compare two strings with ==, concatenate them with +, and print them with <<. None of this magic comes from the language itself — it comes from operator overloading: the ability to give standard operators a meaning for your own class types. When done well, operator overloading makes code that uses your class read as naturally as code that uses fundamental types. The key rule is that overloaded operators should behave exactly as their built-in counterparts — no surprises.
What operator overloading is
To define an operator for your class, you write a function whose name is the operator keyword followed by the operator symbol. The function is called automatically whenever C++ evaluates an expression containing that operator with an operand of your class type. The expression box1 < box2 is exactly equivalent to the function call box1.operator<(box2) — they compile to identical code, and you can even write the function-call form explicitly if it helps you understand what is happening.
Without overloading, comparing two Box objects by volume requires a named method call:
// Without operator overloading — verbose and unlike how built-in types work:
if (nextBox->compare(*largestBox) > 0)
largestBox = nextBox;
// With operator overloading — reads like comparing any two values:
if (*nextBox > *largestBox)
largestBox = nextBox;The second form is not just shorter — it is clearer. Any reader immediately understands that one box is being compared with another, without needing to know that compare()returns a positive integer to mean “greater.”
Member operator functions — binary operators
A binary operator (one that takes two operands) implemented as a class member function has exactly one parameter — the right operand. The left operand is always the object the function is called on, accessible via this. Because comparison operators do not modify either operand, both the parameter and the function itself are const. Here is the less-than operator for a Box class that compares volumes:
class Box {
public:
double volume() const { return m_length * m_width * m_height; }
// operator< as a member: left operand is *this, right operand is aBox
bool operator<(const Box& aBox) const {
return volume() < aBox.volume();
}
private:
double m_length {1.0}, m_width {1.0}, m_height {1.0};
};
// Usage — reads like a built-in comparison:
Box small {1.0, 2.0, 3.0};
Box large {4.0, 5.0, 6.0};
if (small < large)
std::println("small fits inside large");The diagram: in the expression small < large, small is the object this points to, and large is passed as aBox. The function is declared const because evaluating a comparison should never change the box.
Nonmember operator functions — when member is not possible
A member operator function always puts your class as the left operand. That works for box < 25.0 but not for 25.0 < box — because the double on the left cannot have members added to it. For these cases you write the operator as an ordinary function with two explicit parameters. Two of the most important operator overloads — comparing with a non-class type on the left, and stream insertion — must be nonmember functions:
// Member: handles box < 25.0
bool Box::operator<(double value) const { return volume() < value; }
// Nonmember: handles 25.0 < box (double is on the left)
bool operator<(double value, const Box& box) { return value < box.volume(); }
// Stream insertion must be nonmember — you cannot add members to std::ostream:
std::ostream& operator<<(std::ostream& stream, const Box& box) {
stream << "Box(" << box.getLength() << ", "
<< box.getWidth() << ", "
<< box.getHeight() << ')';
return stream; // return stream reference to allow chaining: cout << a << b
}
// Usage:
std::cout << small << " is less than " << large << std::endl;The stream insertion function takes an std::ostream& reference as its first parameter (the stream), and returns that same reference so multiple <<calls can be chained together in one expression. Because we access the box's dimensions through public getter functions, no friend declaration is needed.
The spaceship operator — full comparison with one function (C++20)
Before C++20, supporting all six comparison operators (<, >, <=, >=, ==, !=) required six separate functions. C++20 introduced the three-way comparison operator <=>(nicknamed the “spaceship operator”). When you define <=>, the compiler automatically synthesizes <, >, <=, and >= for you. Add == and the compiler generates != as well — seven operators from just two definitions.
#include <compare> // for std::partial_ordering
class Box {
public:
// Returns partial_ordering because floating-point values can be NaN
std::partial_ordering operator<=>(const Box& other) const {
return volume() <=> other.volume();
}
// Equality compares dimensions, not volume (1x2x3 ≠ 1x1x6 even though same volume)
bool operator==(const Box& other) const {
return m_length == other.m_length
&& m_width == other.m_width
&& m_height == other.m_height;
}
private:
double m_length {1.0}, m_width {1.0}, m_height {1.0};
};
// All seven operators now work:
Box a {2.0, 2.0, 3.0};
Box b {1.0, 3.0, 5.0};
bool r1 = a < b; // true (volume 12 < 15)
bool r2 = a >= b; // false
bool r3 = a != b; // true (different dimensions)
bool r4 = a == b; // falseThe three-way operator returns an ordering type rather than bool. For most classes you will use std::strong_ordering (total order, no ties unless objects are identical) or std::partial_ordering (some pairs may be unordered, as with floating-point NaN). The compiler synthesizes the relational operators using comparisons against the ordering type's named constants.
If you want the compiler to generate <=> as a member-wise lexicographic comparison (comparing each member variable in declaration order), you can default it — and the compiler will simultaneously default == as well:
class Point {
public:
auto operator<=>(const Point&) const = default;
// This single line defaults both <=> and ==, giving all seven comparison operators.
private:
double m_x {}, m_y {}, m_z {};
};Arithmetic operators — producing new objects
Arithmetic operators like + create and return a new object — they do not modify either operand. The idiomatic approach is to implement the compound assignment version (+=) first, since it modifies the left operand in place and is the more fundamental operation, then implement the plain arithmetic operator in terms of it. The compound assignment operator modifies *this and returns a reference to *this so it can be used in larger expressions.
class Box {
public:
// += modifies *this in place; returns a reference to allow chaining
Box& operator+=(const Box& aBox) {
m_length = std::max(m_length, aBox.m_length);
m_width = std::max(m_width, aBox.m_width);
m_height += aBox.m_height;
return *this;
}
// + creates a copy, applies +=, and returns the new value
Box operator+(const Box& aBox) const {
Box copy {*this};
copy += aBox;
return copy;
}
private:
double m_length {1.0}, m_width {1.0}, m_height {1.0};
};
Box box1 {20.0, 15.0, 7.0};
Box box2 {25.0, 10.0, 14.0};
Box combined = box1 + box2; // length=25, width=15, height=21
box1 += box2; // same result, but modifies box1 directlyBecause operator+ returns a new object (not a reference to an existing one), its return type is Box by value. Never return a reference from an arithmetic operator — the local object you created would be destroyed when the function returns, leaving a dangling reference.
Restrictions and design guidelines
Operator overloading has a few hard rules that the language enforces, plus some important guidelines that experienced programmers follow to keep code readable:
Hard rules — enforced by the compiler
- ✗ You cannot invent new operators (e.g., no ??, ===, or <>)
- ✗ You cannot change the number of operands, associativity, or precedence of an operator
- ✗ You cannot override operators for built-in types — at least one operand must be a class type
Strong guidelines — for readable, predictable code
- → Overloaded operators must behave like their built-in counterparts — no surprises
- → Never overload && or || — the overloaded versions cannot short-circuit like the built-in versions
- → Implement + in terms of += (not the other way around) — it avoids code duplication
- → Comparison and read-only operators should be const member functions
- → Assignment operators (=, +=, etc.) must be member functions; they return Box& (reference to *this)
- → Arithmetic operators (+, -, etc.) return by value, never by reference
Member vs. nonmember — which to use
| Operator category | Form | Reason |
|---|---|---|
| Assignment: =, +=, -=, … | Member only | Must modify *this; compiler requires member for = |
| Comparison: <, >, ==, <=>, … | Member (preferred) | Left operand is always the class type |
| Arithmetic: +, -, *, … | Member (preferred) | Naturally part of the class interface |
| Stream insertion: << | Nonmember | Cannot add members to std::ostream |
| Mixed-type arithmetic (left ≠ class) | Nonmember | Member can't have a non-class left operand |
Key rules to remember
operator functions are just functions with a special name
box1 < box2 and box1.operator<(box2) are identical. Understanding this makes everything else about overloading obvious.
A member binary operator has one parameter; a nonmember binary operator has two
For member functions, the left operand is *this. For nonmember functions, both operands are explicit parameters.
Use operator<=> to get all six comparison operators from two definitions (C++20)
Define <=> (returns an ordering type) and == (returns bool). The compiler synthesizes <, >, <=, >= from <=>, and != from ==.
Implement op= (compound assignment) first, then build op from it
Box operator+(const Box& b) const { Box copy{*this}; copy += b; return copy; } — no logic duplication.
Arithmetic operators return by value; assignment operators return by reference
Returning a reference to a local variable is undefined behavior. Assignment operators return *this by reference to enable chaining (a += b += c).
Never overload && or || for a class
Overloaded logical operators lose short-circuit evaluation — both operands are always evaluated, which surprises callers and can cause bugs.