| Lesson 9 | Overloading binary operators |
| Objective | Write member functions in C++23 to overload binary operators |
Operator overloading lets your user-defined types behave like built-in types in expressions.
In modern C++23, the goal is not “make everything overloadable.” The goal is to create small, readable,
predictable types whose operators follow the same expectations programmers already have for
+, -, *, ==, and friends.
This lesson shows how to overload binary operators using:
*this)friend when they need private access)
A binary operator has two operands. When you overload it as a member function, the left operand becomes the implicit object (*this), and the right operand is the only explicit parameter:
a + b becomes a.operator+(b)a + b becomes operator+(a, b)That difference matters most for symmetrical operators, where you expect both operands to be treated the same (e.g., +, *, ==, !=).
With a member overload, the left operand is “special” because it must already be your type (or implicitly
convertible to it). With a non-member, both sides participate equally in overload resolution and conversions.
For symmetrical binary operators, it is often normal and preferred to implement the operator as a non-member, frequently as a friend when private data is involved. This gives both operands the same rules for conversions and keeps your API more flexible.
In C++23, don’t be afraid of friend when it is narrowly used to implement a small set of operators. The key is to keep the friend surface area minimal and ensure the operator preserves class invariants.
A friend declaration grants access to private members, but it does not automatically provide a visible declaration to users in all environments. The portable approach is:
Continuing the Clock style example: implement operator+ as a non-member so both operands are treated symmetrically.
class Clock {
public:
Clock() = default;
explicit Clock(unsigned long seconds) : tot_secs(seconds) {}
friend Clock operator+(const Clock& lhs, const Clock& rhs);
private:
unsigned long tot_secs{0};
};
Clock operator+(const Clock& lhs, const Clock& rhs) {
return Clock(lhs.tot_secs + rhs.tot_secs);
}
Modern improvements versus legacy style:
const Clock& parameters to avoid unnecessary copies.
(Copying small types can be fine, but this is a clean default habit.)
Clock object to preserve value semantics. This is the usual expectation for arithmetic
operators: they don’t mutate operands.
tot_secs) private so operators cannot violate invariants accidentally.
A member overload is appropriate when the left operand must be your type and the operation is naturally “owned”
by the class. In that case, remember the implicit first operand is *this.
class Clock {
public:
Clock() = default;
explicit Clock(unsigned long seconds) : tot_secs(seconds) {}
Clock operator-(const Clock& rhs) const {
return Clock(tot_secs - rhs.tot_secs);
}
private:
unsigned long tot_secs{0};
};
Note: - is also symmetrical in the “two operands” sense, but it is not commutative, and designs vary.
The bigger issue is still conversions: if you want lhs and rhs treated identically for
conversions, a non-member is often the safer choice.
+, consider also implementing
operator+=. A common pattern is implement += as the core operation, then define
+ in terms of it.
== is overloaded, also overload !=, or rely on C++20+ defaulted comparisons where it fits your type design.
Click the Exercise link below to practice overloading several arithmetic binary operators in a class that implements a set.
Overloading Binary Operators - Exercise