C++ Operator Overloading: Member vs Free Function Rules
An overloaded operator in C++ is a function named operator plus the symbol. When to make it a member or a free function, what cannot be overloaded, and ++.
- Course: C++ study plan
- Module: Operator overloading
- Kind: Lesson
- Reading time: 14 min
- Runtime: Clang 18 · C++20
How does operator overloading work in C++?
Operator overloading in C++ lets a class give an operator such as +, == or << a meaning for its own type: a + b is compiled as a call to a function named operator+. Precedence, associativity and the number of operands stay fixed, and at least one operand must be a class or enumeration type. Five operators cannot be overloaded: ., .*, ::, ?: and sizeof.
Lesson
a + b means addition for int, double and std::string, and C++ lets it mean addition for your Fraction, Money or Vec2 too: total += price * qty instead of total.add(price.times(qty)). The mechanism is only a function with a strange name, but the rules about which operators, where the function lives and what it returns decide whether the result reads like arithmetic or like a trap. This lesson settles those rules and the one idiom — compound first, binary from compound — that produces a consistent set every time.
What an overloaded operator is
struct Vec2 {
double x = 0;
double y = 0;
};
Vec2 operator+(const Vec2& a, const Vec2& b) {
return Vec2{a.x + b.x, a.y + b.y};
}
Vec2 p{1, 2}, q{3, 4};
Vec2 r = p + q; // the compiler rewrites this as operator+(p, q)
Vec2 s = operator+(p, q); // legal, and exactly the same call
An expression p + q where at least one operand has class or enumeration type is looked up as a call to a function named operator+. Nothing else changes: precedence, associativity and arity are fixed by the grammar, so a + b * c still multiplies first however you define *, and you cannot invent ** or give + three operands. Nor can you redefine an operator on built-in types alone — int operator+(int, int) is an error.
Almost every operator can be overloaded: the arithmetic and bitwise set, the comparisons, << and >>, [], (), ->, =, the compound assignments, ++/--, !, , and the conversion operators (lesson 5). Five cannot: ., .*, ::, ?: and sizeof. Three that can be overloaded should not be — &&, || and , — because an overloaded version is an ordinary function call: both operands are always evaluated, and the short-circuit guarantee is gone.
Member or free function
An operator can be a member, in which case the left operand is *this and the parameter list holds only the right one, or a free function taking both operands:
class Money {
public:
explicit Money(long long cents) : cents_(cents) {}
long long cents() const { return cents_; }
Money& operator+=(const Money& o) { cents_ += o.cents_; return *this; } // member: mutates *this
friend Money operator+(Money a, const Money& b) { return a += b; } // hidden friend: a free function
private:
long long cents_;
};
Money operator*(const Money& m, long long k) { return Money(m.cents() * k); }
Money operator*(long long k, const Money& m) { return m * k; } // 3 * price needs a free function
The choice is not taste. Four operators must be members: =, [], () and -> (and the conversion operators). Everything that mutates its left operand — the compound assignments, ++, -- — is naturally a member too. Symmetric binary operators — +, -, *, ==, <, << — belong outside the class, for one concrete reason: a member operator's left operand is never subject to implicit conversion. With Money::operator*(long long) as a member, price * 3 compiles and 3 * price does not, because 3 is an int and there is no int::operator*. A free function treats both sides alike. In std::cout << m the left operand is a std::ostream, a class you cannot add members to, so operator<< is always free (lesson 3).
A free operator that needs private members is declared friend inside the class, and defining it there — as operator+ above — gives the hidden friend idiom: a non-member (*this does not exist inside it) that is found only when an argument is a Money, which keeps it out of unrelated overload sets and error messages. It is the modern default for a class's binary operators.
Compound first, binary from compound
Write += as the member that does the work; write + as a free function that copies its left operand, applies += and returns the copy:
Vec2& Vec2::operator+=(const Vec2& o) { x += o.x; y += o.y; return *this; }
Vec2& Vec2::operator*=(double k) { x *= k; y *= k; return *this; }
Vec2 operator+(Vec2 a, const Vec2& b) { return a += b; } // a is the copy; += returns it by reference
Vec2 operator*(Vec2 v, double k) { return v *= k; }
Vec2 operator*(double k, Vec2 v) { return v *= k; }
Taking the left operand by value is deliberate: the operator needs a copy to modify anyway, and a by-value parameter lets the compiler build that copy straight from a temporary, so (a + b) + c moves the intermediate rather than copying it. The idiom guarantees a + b and a += b can never disagree — there is one implementation — and the same shape serves -, *, /, % and the bitwise pairs.
Unary operators and increment
Unary minus, ! and ~ take no parameter as members (Money operator-() const) or one as free functions, return a new value and leave the operand alone. Increment and decrement come in two spellings that the language tells apart by a dummy parameter:
class Counter {
public:
Counter& operator++() { ++n_; return *this; } // prefix: ++c, returns the updated object
Counter operator++(int) { Counter old = *this; ++*this; return old; } // postfix: c++, returns the OLD value
int value() const { return n_; }
private:
int n_ = 0;
};
The int is never used; it exists so the two overloads have different signatures. Prefix returns a reference to the object; postfix must return the previous value, which costs a copy — the reason ++it is preferred over it++ in loops. Implement postfix through prefix so the two cannot drift.
Return types
| Operator | Return | Why |
|---|---|---|
+ - * / %, unary - | by value | A new value; the operands are untouched |
==, < | bool | A comparison, not a value of the type |
+= -= *= /=, =, prefix ++ | T& (*this) | The same object, updated; chains like the built-ins |
postfix ++/-- | T by value | The old value exists nowhere else |
<< on a stream | std::ostream& | So os << a << b chains |
[] | T& / const T& | The element itself, assignable (lesson 4) |
Never return a reference to a local — it dangles the moment the function returns (Module 6, lesson 4). Mark every operator that does not modify *this as const, or it cannot be used on a const Money or through a const& parameter.
Do as the ints do
The reader of a + b brings expectations from the built-in types, and an overload that violates them is worse than a named function. + adds and does not mutate; == is symmetric and agrees with !=; < is a strict order consistent with == (lesson 2); += does what + then = would do. A Matrix whose * is element-wise, a String whose - removes a substring — each compiles, and each costs every future reader a trip to the definition. The standard library breaks the rule once, deliberately: std::filesystem::path defines / as join because dir / "file.txt" reads like a path. If your overload needs a comment to say what it does, write a named function.
Pitfalls
| Mistake | What happens |
|---|---|
operator* as a member, then 2 * v | No conversion on a member's left operand: no match |
+ written independently of += | The two drift; one gets fixed, the other does not |
operator+= returning void or by value | a += b += c fails, or copies |
Postfix ++ implemented as prefix | c++ yields the new value; loops off by one |
No const on operator== or operator-() | Cannot be used on const objects or const& parameters |
friend operator declared but never defined | Linker error: undefined reference |
Key takeaways
- An overloaded operator is a function named
operator@; precedence, arity and the built-in meanings are untouchable. =,[],(),->and conversions must be members; symmetric binary operators should be free functions (hidden friends), so conversions apply to both operands.- Write
+=as the member that works, and+as a one-line free function taking its left operand by value and returninga += b. - Prefix
++returnsT&; postfix takes a dummyintand returns the old value — implement it via prefix. - Return by value for results,
*thisby reference for assignments; mark non-mutating operatorsconst. - Do as the ints do: an operator that needs a comment should be a named function.
Common questions
Should an overloaded operator be a member or a free function?
It depends on the operator. =, [], () and -> must be members, and operators that modify their left operand, such as += and ++, naturally are. Symmetric binary operators like +, == and << should be free functions, often hidden friends, because a member's left operand never gets an implicit conversion — 3 * price would not compile.
Which operators cannot be overloaded in C++?
Five: member access ., pointer-to-member access .*, scope resolution ::, the conditional operator ?: and sizeof. You also cannot invent operators such as **, change an operator's precedence or arity, or redefine an operator for built-in types alone. &&, || and , can be overloaded but should not be, because the overloads lose short-circuit evaluation.
How do you overload prefix and postfix ++ in C++?
Prefix operator++() increments and returns *this by reference. Postfix operator++(int) takes an unused int parameter that only distinguishes the two signatures, saves a copy, increments through the prefix version and returns the old value by value. That copy is why ++it is preferred to it++ in loops.
How do you implement operator+ using operator+=?
Write += as a member that updates *this and returns a reference, then write + as a free function that takes its left operand by value, applies += to that copy and returns it: return a += b;. There is one implementation, so a + b and a += b can never disagree, and a temporary left operand is moved rather than copied.
What should an overloaded operator return?
Arithmetic operators such as + return a new value by value; comparisons return bool; assignment, compound assignment and prefix ++ return *this by reference so they chain; postfix ++ returns the old value by value; stream << returns the stream by reference. Never return a reference to a local variable.
Exercises
Fraction arithmetic
Complete the Fraction class so that it supports +, -, * and /. Every fraction is kept normalised: the sign lives in the numerator (the denominator is always positive) and numerator and denominator are divided by their std::gcd, so 2/4 is stored as 1/2, 1/-3 as -1/3 and 0/7 as 0/1. Write +=, -=, *= and /= as members that normalise after the arithmetic, then the four binary operators as free functions that take their left operand by value and return a += b (and so on) — the compound-first idiom.
Input: an integer n, then n lines of the form a/b op c/d where op is one of + - * /, the four numbers are integers and no denominator is zero. The input never divides by a zero fraction. Output: n lines: the two operands normalised, the operator, and the result as p/q, e.g. 1/2 + 1/3 = 5/6.
3
1/2 + 1/3
2/4 - 1/-3
3/4 * 2/3
prints
1/2 + 1/3 = 5/6
1/2 - -1/3 = 5/6
3/4 * 2/3 = 1/2
Use long long for the parts: the intermediate products can exceed int.
Displacement on a grid
Give the Vec2 struct (integer x and y) the operators a vector needs: +=, -= and *= by an integer scalar as members; +, -, v * k and k * v as free functions derived from them; unary - as a const member; and a length() member returning std::sqrt of the squared length as a double. Then simulate a walk: a start position and a series of steps, each a velocity vector and a number of ticks, so that the position advances by ticks * velocity — use the scalar-on-the-left form to prove it exists.
Input: the start x y, then n, then n lines of vx vy ticks (all integers; n may be 0). Output: the position after each step as (x, y); then displacement (dx, dy) (final minus start), return (-dx, -dy) (the unary minus of the displacement), and distance <length of the displacement, one decimal>.
0 0
2
3 4 1
-1 2 2
prints
(3, 4)
(1, 8)
displacement (1, 8)
return (-1, -8)
distance 8.1In this module: Operator overloading
← Checkpoint — Inheritance and polymorphism · Comparison and the spaceship operator →