Lesson 24 • Advanced
Operator Overloading
By the end of this lesson you'll be able to make your own classes behave like built-in types — adding them with +, comparing them with == and <, indexing them with [], and printing them with cout << obj — while getting return types and const-correctness right.
What You'll Learn
- Overload arithmetic operators (+, -, *) on your own class
- Overload comparison operators (== and <) const-correctly
- Write compound assignment (+=) that returns *this for chaining
- Give your class subscript access with operator[]
- Print objects with a friend operator<< stream insertion
- Choose member vs non-member, and know the rule of three/five
💡 Real-World Analogy
Think of an operator like + as a universal plug socket. The built-in types (int, double) already fit it. Operator overloading is wiring an adapter so your own type fits the same socket — once Money + Money is wired up, the rest of the language (printing, sorting, totalling) "just works" with your type the way it does with numbers. You're not inventing new syntax; you're teaching a familiar symbol what it means for your class. The skill is wiring the adapter correctly: returning the right thing, and not changing what shouldn't change.
📊 Operators You'll Overload
| Operator | Does | Returns | Member? |
|---|---|---|---|
| + - * | Make a new value | by value (new object) | prefer non-member |
| == < | Compare two values | bool | prefer non-member |
| += | Change this object | *this by reference | member |
| [ ] | Index into the object | element by reference | must be member |
| << | Print to a stream | the stream by reference | must be non-member |
The hardest part isn't the syntax — it's the return type. Operators that build a new value return by value; operators that change the object return *this by reference. Keep that distinction and most bugs disappear.
1. A Fully-Wired Class: Money
Here's a complete, correct example that overloads every operator this lesson covers on a Money type (stored as whole pence so there's no rounding drift). Read every comment, run it, and check the output. Notice the four shapes you'll reuse forever: arithmetic returns a new object by value, compound assignment returns *this by reference, comparison returns bool and is const, and operator<< is a non-member friend that returns the stream.
Worked example: the Money class
Read every comment, run it, and check the output matches.
#include <iostream>
using namespace std;
// A Money type that stores a whole number of pence (no rounding drift).
class Money {
public:
long pence; // 1234 means £12.34
Money(long p = 0) : pence(p) {}
// (1) ARITHMETIC as a MEMBER. 'const' = "I don't change this object".
// We take the right-hand side by const reference (no copy).
// We RETURN BY VALUE because the sum is a brand-new Money.
Money operator+(const Money& rhs) const {
...2. Member vs Non-Member — How to Choose
When you write an operator as a member, the left operand is always the object itself (*this), so it takes one fewer parameter. That's perfect for operators that belong to the object: =, [], (), ->, and the compound assignments like +=. A non-member function is symmetric: both operands are ordinary parameters, so it works even when the left side isn't your class. That's why operator<< must be a non-member — its left operand is cout, not your type — and why + and == are usually non-members too, so 2.0 * vec works as well as vec * 2.0.
🔎 Deep Dive: why friend for operator<<
A member operator's left operand is fixed as your object. But cout << m has cout on the left, so operator<< can't be a member — it's a free function taking (ostream&, const Money&). You mark it friend inside the class only so it can read private fields; it returns ostream& so chained << works.
// inside the class, granting access to privates:
friend std::ostream& operator<<(std::ostream& os, const Money& m);
// defined outside as a normal non-member:
std::ostream& operator<<(std::ostream& os, const Money& m) {
return os << "£" << (m.pence / 100.0); // returns the stream -> chains
}3. Your Turn: Arithmetic & <<
Time to wire up your own type. The Vector2 below is almost finished — fill in the blanks marked ___ using the hints, then run it. Remember: operator+ and operator* build a new vector, so they return one by value.
🎯 Your turn: Vector2 arithmetic
Fill in the ___ blanks, then check your output against the expected lines.
#include <iostream>
using namespace std;
class Vector2 {
public:
double x, y;
Vector2(double x = 0, double y = 0) : x(x), y(y) {}
// 🎯 YOUR TURN — fill each ___ then press "Try it Yourself".
// 1) operator+ adds two vectors and returns a NEW Vector2 (by value).
Vector2 operator+(const Vector2& o) const {
return Vector2(___, ___); // 👉 x + o.x , y + o.y
}
// 2) operator* scales the vector by a number (member: vec * 3).
Vector2 operator*(double
...4. Subscript [], Compound +=, and ==
Three more shapes to lock in. operator[] returns the element by reference so callers can assign into it (s[2] = 30). operator+= changes the object, so it returns *this by reference — that's what lets you chain (s += 10) += 20. And operator== compares field-by-field and returns bool. Fill in the blanks below.
🎯 Your turn: [], += and ==
Return references where the comment tells you to, then run and self-check.
#include <iostream>
using namespace std;
class Stack3 {
public:
int items[3] = {0, 0, 0};
int count = 0;
void push(int v) { if (count < 3) items[count++] = v; }
// 🎯 YOUR TURN — fill each ___.
// 1) operator== is true when both have the SAME count and same items.
bool operator==(const Stack3& o) const {
if (count != o.count) return false;
for (int i = 0; i < count; i++)
if (items[i] != o.items[i]) return ___; // 👉 false
return
...🔎 Deep Dive: operator= and the Rule of Three / Five
operator= (copy assignment) is the one operator with serious hidden danger. If your class manages a raw resource — heap memory, a file handle — and you write a custom destructor, you almost certainly also need a custom copy constructor and copy assignment operator. That's the Rule of Three: those three go together. The compiler's default versions copy pointers shallowly, so two objects end up owning the same memory and both try to free it — a double-free crash.
C++11 adds the move constructor and move assignment, extending it to the Rule of Five. The best advice for beginners is the Rule of Zero: store your data in types that already manage themselves — std::string, std::vector, std::unique_ptr — and the compiler-generated operator= is automatically correct, so you write none of the five.
// Rule of Five signatures (only if you manage a raw resource):
class Buffer {
~Buffer(); // destructor
Buffer(const Buffer&); // copy constructor
Buffer& operator=(const Buffer&); // copy assignment
Buffer(Buffer&&) noexcept; // move constructor
Buffer& operator=(Buffer&&) noexcept; // move assignment
};
// Rule of Zero: use std::vector/std::string instead -> write none of these.Pro Tips
- 💡 Implement
+in terms of+=: writeoperator+=first, thena + bas{ T r = a; r += b; return r; }. Less code, no drift. - 💡 Define
!=as!(a == b)and keep<consistent with==— never write them independently. - 💡 Mark read-only operators
constand take big parameters byconst&. It enables use on const objects and avoids copies. - 💡 C++20 spaceship:
auto operator<=>(const T&) const = default;generates all six comparisons from one line.
Common Errors (and the fix)
- Returning by reference instead of value (or vice-versa):
operator+builds a new object, so return it by value — returning a reference to a local is a dangling reference and undefined behaviour.operator+=changes the existing object, so return*thisby reference. - Forgetting
const-correctness: ifoperator+oroperator==isn't markedconst, you can't use it on aconstobject or aconst¶meter — "passing 'const T' as 'this' argument discards qualifiers". - Asymmetric
==: defining==but not keeping!=consistent (or comparing only some fields) breaksstd::find,std::set, and sorting. Define!=as!(a == b). - Making
operator<<a member: "operator<<must take exactly two arguments" — its left operand is the stream, so it has to be a non-member (afriendif it reads privates). - Forgetting to return the stream from
<<: ifoperator<<returnsvoid, thencout << a << bwon't compile — returnostream&so it can chain.
📋 Quick Reference
| Operator | Typical signature | Returns |
|---|---|---|
| + | T operator+(const T&) const | new T by value |
| += | T& operator+=(const T&) | *this by reference |
| == | bool operator==(const T&) const | bool |
| < | bool operator<(const T&) const | bool |
| [ ] | E& operator[](int) | element by reference |
| << | ostream& operator<<(ostream&, const T&) | the stream (non-member) |
Frequently Asked Questions
Q: Should an operator be a member function or a non-member?
Rule of thumb: =, [], (), and -> MUST be members. Compound assignment (+=, -=) should be members because they change the left operand. Symmetric binary operators like +, -, *, and == are best as non-members (often using a friend) so that mixed types work on either side, e.g. 2.0 * vec as well as vec * 2.0. The stream operators << and >> MUST be non-members, because their left operand is the stream, not your class.
Q: Why is operator<< written as a friend?
operator<< takes the output stream (ostream&) as its LEFT operand, so it cannot be a member of your class — a member's left operand is always the object itself. It is written as a free function. You mark it 'friend' inside the class only so it can read the class's private members; if everything it prints is public, it doesn't even need to be a friend.
Q: When do I return by value, by reference, or a const reference?
Return BY VALUE when you create a brand-new object: operator+ and operator* produce a new sum/product. Return *this BY REFERENCE from operators that modify the object (operator+=, operator=) so calls can chain. Return a reference from operator[] so callers can assign into the slot (arr[0] = 9). Take parameters by const reference to avoid copies, and mark read-only operators const.
Q: What is the rule of three / five and what does it have to do with operator=?
If your class manages a resource (raw memory, a file handle) and you need a custom destructor, copy constructor, OR copy assignment operator (operator=), you almost certainly need all three — that's the Rule of Three. C++11 adds the move constructor and move assignment, making it the Rule of Five. The safest path is the Rule of Zero: store members in types that already manage themselves (std::string, std::vector, smart pointers) so the compiler-generated operator= is correct and you write none of them.
Q: Why must operator== be symmetric and consistent?
If a == b is true, then b == a must also be true, and == should agree with !=. Asymmetric or inconsistent comparisons break sorting, std::set/std::map, and std::find in ways that are very hard to debug. Implement == once, define != as !(a == b), and (pre-C++20) keep < consistent with them. In C++20 you can let the compiler do this with operator<=> (the 'spaceship' operator).
Mini-Challenge: a Temperature class
No blanks this time — just a brief and an outline. Build a Temp class that supports +, <, and printing with <<. Run it and check your output against the example in the comments. This is exactly the shape of a real value type.
🎯 Mini-Challenge: overload +, <, and << on Temp
Write the operators yourself, then check your output against the example.
#include <iostream>
using namespace std;
// 🎯 MINI-CHALLENGE: a Temperature class (stored in Celsius)
// 1. A class 'Temp' with a double 'celsius' and a constructor.
// 2. operator+ : add two Temps, return a NEW Temp (by value, const).
// 3. operator< : true if this is colder than the other (const).
// 4. operator<< : a NON-MEMBER (friend if it needs privates) that
// prints like "21.5°C" and RETURNS the stream.
// 5. In main: make Temp(18) and Temp(4), add them, and compare
...🎉 Lesson Complete
- ✅ Arithmetic (
+ - *) returns a new object by value - ✅ Compound assignment (
+=) returns*thisby reference so it chains - ✅ Comparison (
==,<) returnsbooland isconst; keep!=consistent with== - ✅
operator[]returns the element by reference so you can assign into it - ✅
operator<<is a non-member friend that returns the stream - ✅ Member for
=/[]/()/+=; non-member for symmetric+/==/<< - ✅ Mind the Rule of Three/Five for
operator=— or follow the Rule of Zero - ✅ Next lesson: Concurrency in C++ — threads, futures, promises, and async
Sign up for free to track which lessons you've completed and get learning reminders.