Skip to main content
    Courses/C++/Operator Overloading

    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

    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.

    Worked example: the Money class

    Read every comment, run it, and check the output matches.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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 +=: write operator+= first, then a + b as { 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 const and take big parameters by const&. 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 *this by reference.
    • Forgetting const-correctness: if operator+ or operator== isn't marked const, you can't use it on a const object or a const& parameter — "passing 'const T' as 'this' argument discards qualifiers".
    • Asymmetric ==: defining == but not keeping != consistent (or comparing only some fields) breaks std::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 (a friend if it reads privates).
    • Forgetting to return the stream from <<: if operator<< returns void, then cout << a << b won't compile — return ostream& so it can chain.

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

    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.

    Try it Yourself »
    C++
    #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 *this by reference so it chains
    • ✅ Comparison (==, <) returns bool and is const; 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.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service