Operator Overloading

Reviewed & published by Brayan K

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.

Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 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

OperatorDoesReturnsMember?
+ - *Make a new valueby value (new object)prefer non-member
== <Compare two valuesboolprefer non-member
+=Change this object*this by referencemember
[ ]Index into the objectelement by referencemust be member
<<Print to a streamthe stream by referencemust 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.

#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 {
        return Money(pence + rhs.pence);     // a fresh object
    }
    Money operator-(const Money& rhs) const {
        return Money(pence - rhs.pence);
    }
    Money operator*(int qty) const {         // price * quantity
        return Money(pence * qty);
    }

    // (2) COMPOUND assignment changes *this, so it returns *this BY REFERENCE
    //     (so you can chain:  a += b += c). Note: NOT const here.
    Money& operator+=(const Money& rhs) {
        pence += rhs.pence;
        return *this;                        // return the SAME object
    }

    // (3) COMPARISON returns bool. Keep == and < const-correct.
    bool operator==(const Money& rhs) const { return pence == rhs.pence; }
    bool operator<(const Money& rhs)  const { return pence < rhs.pence; }
};

// (4) STREAM INSERTION must be a NON-MEMBER (the left operand is 'cout',
//     not your class). We make it a 'friend' so it can read private data,
//     and we return the stream BY REFERENCE so '<<' can chain.
ostream& operator<<(ostream& os, const Money& m) {
    long pounds = m.pence / 100;
    long pennies = m.pence % 100;
    os << "£" << pounds << "." << (pennies < 10 ? "0" : "") << pennies;
    return os;                               // chaining works because we return os
}

int main() {
    Money coffee(350);     // £3.50
    Money cake(425);       // £4.25

    Money total = coffee + cake;             // operator+   -> £7.75
    cout << "Total:   " << total << endl;    // operator<<  -> Total:   £7.75

    Money two = coffee * 2;                  // operator*   -> £7.00
    cout << "Two cof: " << two << endl;

    total += Money(50);                      // operator+=  -> £8.25
    cout << "After +50p: " << total << endl;

    cout << "coffee < cake? " << (coffee < cake) << endl;  // operator<  -> 1 (true)
    cout << "coffee == coffee? " << (coffee == coffee) << endl; // operator== -> 1

    return 0;
}

// ✅ Expected output:
//    Total:   £7.75
//    Two cof: £7.00
//    After +50p: £8.25
//    coffee < cake? 1
//    coffee == coffee? 1

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.

#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 s) const {
        return Vector2(___, ___);     // 👉 x * s  ,  y * s
    }
};

// 3) operator<<  prints it. Return the stream so '<<' can chain.
ostream& operator<<(ostream& os, const Vector2& v) {
    return os << "(" << v.x << ", " << v.y << ")";
}

int main() {
    Vector2 a(1, 2), b(3, 4);
    cout << "a + b = " << (a + b) << endl;
    cout << "a * 3 = " << (a * 3) << endl;

    // ✅ Expected output:
    //    a + b = (4, 6)
    //    a * 3 = (3, 6)
    return 0;
}

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.

#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 ___;                                   // 👉 true
    }

    // 2) operator+=  pushes one value, then returns *this so it can chain.
    Stack3& operator+=(int v) {
        push(v);
        return ___;                                   // 👉 *this
    }

    // 3) operator[]  reads/writes slot i. Return BY REFERENCE so x[0] = 9 works.
    int& operator[](int i) {
        return ___;                                   // 👉 items[i]
    }
};

int main() {
    Stack3 s;
    (s += 10) += 20;          // chaining works because += returns *this
    s[2] = 30;                // operator[] returns a reference, so we can assign
    for (int i = 0; i < 3; i++) cout << s[i] << " ";
    cout << endl;

    Stack3 t;
    t += 10; t += 20; t[2] = 30;
    cout << "s == t? " << (s == t) << endl;

    // ✅ Expected output:
    //    10 20 30
    //    s == t? 1
    return 0;
}

🔎 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

Common Errors (and the fix)

📋 Quick Reference

OperatorTypical signatureReturns
+T operator+(const T&) constnew T by value
+=T& operator+=(const T&)*this by reference
==bool operator==(const T&) constbool
<bool operator<(const T&) constbool
[ ]E& operator[](int)element by reference
<<ostream& operator<<(ostream&, const T&)the stream (non-member)

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.

#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 them.
//
// ✅ Expected output (example):
//    18°C + 4°C = 22°C
//    4°C < 18°C? 1

class Temp {
    // your code here
};

int main() {
    // your code here
    return 0;
}

🎉 Lesson Complete

Practice quiz

How should operator+ return its result?

  • By reference to *this
  • By value — it builds a brand-new object
  • By pointer
  • By const reference to a local

Answer: By value — it builds a brand-new object. Arithmetic operators like + create a new value (a fresh object), so they return by value.

What should operator+= return, and how?

  • A new object by value
  • void
  • *this by reference, so calls can chain
  • A copy of the right-hand side

Answer: *this by reference, so calls can chain. Compound assignment modifies the existing object and returns *this by reference, enabling chains like (a += b) += c.

Why must operator<< be a non-member function?

  • Members can't return references
  • Its left operand is the stream (cout), not your class
  • It is too large for a class
  • Streams forbid member operators

Answer: Its left operand is the stream (cout), not your class. cout << m has cout on the left, so operator<< can't be a member (a member's left operand is always *this); it's a free function.

Why is operator<< often marked friend inside the class?

  • To make it run faster
  • So it can read the class's private members
  • Because friends are required for all operators
  • To allow it to modify the stream

Answer: So it can read the class's private members. friend grants the free operator<< access to private fields; if it only prints public data it doesn't even need to be a friend.

What must operator<< return so that cout << a << b chains?

  • void
  • bool
  • ostream& (the stream by reference)
  • a copy of the object

Answer: ostream& (the stream by reference). Returning ostream& lets the next << operate on the same stream, so chaining works.

Why does operator[] return the element by reference (E&)?

  • To avoid copying the whole container
  • So callers can assign into the slot, e.g. s[2] = 30
  • Because [] must return void
  • To make the index bounds-checked

Answer: So callers can assign into the slot, e.g. s[2] = 30. Returning a reference lets s[2] = 30 write into the element; returning by value would only give a temporary copy.

For a read-only comparison like operator==, what qualifier should it carry?

  • static
  • const
  • virtual
  • friend

Answer: const. Comparison operators don't modify the object, so mark them const so they work on const objects and const& parameters.

Given a Money type storing whole pence, what does Money(350) + Money(425) represent?

  • £3.50
  • £4.25
  • £7.75
  • £8.25

Answer: £7.75. 350p + 425p = 775p = £7.75; operator+ adds the pence and returns a new Money.

What is the Rule of Zero?

  • Never write any operators
  • Store data in self-managing types (string, vector, smart pointers) so the compiler-generated special members are correct
  • Always write all five special members by hand
  • Use only static member functions

Answer: Store data in self-managing types (string, vector, smart pointers) so the compiler-generated special members are correct. The Rule of Zero: rely on members that manage themselves so you write none of the five special members and operator= is automatically correct.

In C++20, what single declaration can generate the comparison operators automatically?

  • auto operator<=>(const T&) const = default;
  • bool operator==(const T&) = 0;
  • operator compare() default;
  • using std::compare;

Answer: auto operator<=>(const T&) const = default;. The C++20 spaceship operator <=> defaulted generates the comparisons consistently from one line.

Continue this course

Frequently asked questions

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.

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.

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.

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.

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).

Related lessons