C++ / CLASSES AND OBJECT LIFECYCLE
Operator overloading and the operators worth defining
Choose which operators a class should overload, place them as members or non-members for the right reasons, and implement +=, +, ==, <=> and << correctly.
What you will learn
- Write symmetric binary operators as non-members so the left operand can convert too
- Implement += as a member, then define + from it by taking the left operand by value
- Tell prefix from postfix ++ apart by the unused int parameter on the postfix form
- Default operator<=> to get <, <=, > and >= from member-wise comparison
Understanding Operator overloading and the operators worth defining
An overloaded operator is a function whose name happens to be punctuation. When the compiler sees a + b and either operand is a class type, it collects every operator+ in scope, members of a's class plus non-members found by ordinary and argument-dependent lookup, and runs the same overload resolution it would run for plus(a, b). What overloading cannot touch is the grammar: precedence, associativity and the number of operands are fixed, so a + b * c still multiplies first, and an overloaded && evaluates both sides because short-circuiting belongs to the built-in operator, not to a function call.
The member versus non-member choice is not a matter of style. A member operator's left operand is *this, and no user-defined conversion is ever applied to it, so with a member operator+ the expression wallet + 250 compiles while 250 + wallet does not. Symmetric operators, meaning arithmetic and comparison, therefore belong outside the class where both operands are ordinary parameters and both can convert. The operators the language forces to be members are =, [], (), -> and conversion functions; << must be a non-member because its left operand is a stream you did not write.
Pick the set from what the type actually is rather than from what is possible. If values add, write operator+= as a member, since it needs private access and mutates the left operand, and derive operator+ from it as a non-member that takes its left operand by value, so the arithmetic lives in exactly one place. If two values can be equal write operator==, and if they can be ordered add operator<=>; the compiler then synthesizes !=, <, >, <= and >= from those two. Add operator<< so a failing test prints something readable, then stop, because operator&&, operator, and a clever reuse of % for a domain meaning force every reader to look up what the punctuation does.
<iostream>
class Money {
public:
Money() = default;
Money(long long cents) : cents_{cents} {} // implicit on purpose: see notes
Money& operator+=(const Money& rhs) { // member: left operand is always a Money
cents_ += rhs.cents_;
return *this;
}
long long cents() const { return cents_; }
private:
long long cents_ = 0;
};
// Non-member: both operands are parameters, so both are allowed to convert.
Money operator+(Money lhs, const Money& rhs) { // lhs by value = the result being built
lhs += rhs;
return lhs;
}
bool operator==(const Money& a, const Money& b) {
return a.cents() == b.cents();
}
std::ostream& operator<<(std::ostream& os, const Money& m) { // positive amounts only
os << m.cents() / 100 << '.';
if (m.cents() % 100 < 10) os << '0';
return os << m.cents() % 100;
}
int main() {
Money wallet{1250};
wallet += Money{499};
std::cout << wallet << '\n';
std::cout << wallet + 51 << '\n'; // right operand converts
std::cout << 250 + wallet << '\n'; // left operand converts: needs a non-member
std::cout << std::boolalpha << (wallet == Money{1749}) << '\n';
}
An overloaded operator is an ordinary function with punctuation for a name, so define only the ones whose built-in meaning already matches what your type does, and put the symmetric ones outside the class.
Worked examples
Prefix and postfix increment
Shows how the unused int parameter separates the two ++ overloads and why they return different things.
<iostream>
class Ticket {
public:
explicit Ticket(int n) : n_{n} {}
Ticket& operator++() { // prefix: no parameter, hands back the updated object
++n_;
return *this;
}
Ticket operator++(int) { // postfix: dummy int selects this overload
Ticket old = *this;
++n_;
return old;
}
int number() const { return n_; }
private:
int n_;
};
int main() {
Ticket t{7};
Ticket a = ++t; // t becomes 8, a is the same object's value
Ticket b = t++; // b keeps 8, t becomes 9
std::cout << a.number() << ' ' << b.number() << ' ' << t.number() << '\n';
}
Example explained
Line 1Ticket& operator++() returns a reference because built-in ++i yields the incremented object itself as an lvalue.
Line 2Ticket operator++(int) never uses that int; it exists only so the compiler can tell x++ from ++x, and nothing is passed to it.
Line 3The postfix body copies first and returns the copy, which is exactly why x++ costs a copy and ++x does not.
Line 4Ticket b = t++; leaves b holding 8 while t moves to 9, so the three printed values are 8 8 9.
One <=> instead of six comparisons
A defaulted three-way comparison supplies <, <=, > and >= from member-wise ordering, and a defaulted == supplies == and !=.
<compare>
<iostream>
struct Version { // requires C++20
int major;
int minor;
auto operator<=>(const Version&) const = default; // gives <, <=, >, >=
bool operator==(const Version&) const = default; // gives ==, !=
};
int main() {
Version a{1, 9};
Version b{1, 10};
std::cout << std::boolalpha
<< (a < b) << ' '
<< (a >= b) << ' '
<< (a != b) << ' '
<< (a == Version{1, 9}) << '\n';
std::cout << ((a <=> b) == std::strong_ordering::less) << '\n';
}
Example explained
Line 1The defaulted <=> compares members in declaration order and stops at the first difference, so major decides before minor is examined; Version{2,0} < Version{1,10} is false.
Line 2a >= b is not a function you wrote: the compiler rewrites it as (a <=> b) >= 0.
Line 3== is declared separately because a three-way comparison may be more expensive than an equality test; a != b is then rewritten as !(a == b).
Line 4auto deduces std::strong_ordering here because both members are int, which is why comparing the result against std::strong_ordering::less type-checks.
Important notes
C++20 rewrites a != b as !(a == b) and a > b as (a <=> b) > 0, so == plus <=> is enough. On C++17 and earlier you must write all six by hand, and hand-written ones that disagree produce containers that sort and search inconsistently.
The implicit Money(long long) is what makes 250 + wallet work, but the same conversion also lets wallet + 5 (five cents, not five euros) compile quietly; mark the constructor explicit when that risk outweighs the symmetry.
Common mistakes
Declaring operator+ as a member: wallet + 250 compiles and 250 + wallet does not, and the error talks about no matching operator rather than about the implicit object argument that refused to convert.
Writing postfix operator++ as return ++*this; so that x++ yields the new value; code like arr[i++] then silently reads the wrong element and nothing warns you.
Making operator<< a member function, which makes std::cout << m fail to compile because a member operator's left operand must be your class, leaving only the backwards m << std::cout.
Try it yourself
Change, predict, then run
Write a Fraction class holding int num and int den, implement operator*= as a member and operator* as a non-member built from it, then print Fraction{1,2} * Fraction{2,3} and 3 * Fraction{1,6}. Move operator* inside the class and confirm which of the two expressions stops compiling.
Open the C++ workspaceCheck your understanding
Money has a non-explicit constructor Money(long long). If operator+ is written as a member, Money Money::operator+(const Money&) const, what happens to wallet + 250 and 250 + wallet?
- wallet + 250 compiles, 250 + wallet does not, because no user-defined conversion is applied to the implicit object argument
- 250 + wallet compiles, wallet + 250 does not, because the literal 250 cannot bind to const Money&
- Both fail, because a member operator+ accepts only an argument that is already exactly Money
- Both compile, because the compiler also tries the operands in reverse order
Show answer
Overload resolution will happily use the converting constructor for an argument, so 250 becomes a temporary Money in wallet + 250, which rules out options 2 and 3. The left operand of a member operator is the object the function is called on, and user-defined conversions are never applied there, so 250 + wallet finds no candidate at all. Option 4 is tempting because C++20 does synthesize reversed candidates, but only for ==, != and the relational operators, never for +.