Operator Overloading

C++ · Lesson 18 of 32 6 min read Updated

Built-in types work with operators: you can add two int values with + and compare two strings with <. Operator overloading lets your own classes do the same. A Money class can support price + tax, a Date class can support d1 < d2, and any class can be printed with std::println.

In this lesson you will overload arithmetic and compound assignment operators, get all six comparisons from C++20's <=>, add subscript and call operators, and teach both std::cout and std::println to print your type.

Your first overloaded operators

An overloaded operator is just a function with a special name: operator+, operator+=, operator== and so on. Writing a + b makes the compiler call a.operator+(b).

Money is a good example. Storing rupees in a double causes rounding errors, so this class stores whole paise in an integer.

C++
#include <compare>
#include <print>

class Money {
public:
    Money(long long rupees, int paise) : total_paise_(rupees * 100 + paise) {}

    long long rupees() const { return total_paise_ / 100; }
    int paise() const { return static_cast<int>(total_paise_ % 100); }

    Money& operator+=(const Money& other) {
        total_paise_ += other.total_paise_;
        return *this;                       // allows a += b += c
    }

    Money operator+(const Money& other) const {
        Money result = *this;               // copy, then reuse +=
        result += other;
        return result;
    }

    Money operator*(int times) const {
        Money result = *this;
        result.total_paise_ *= times;
        return result;
    }

    auto operator<=>(const Money&) const = default;   // C++20: all comparisons

private:
    long long total_paise_;
};

int main() {
    Money pen{25, 50};
    Money book{349, 0};

    Money total = pen * 2 + book;          // 51.00 + 349.00
    total += Money{10, 5};

    std::println("Total: Rs. {}.{:02}", total.rupees(), total.paise());
    std::println("pen < book? {}", pen < book);
    std::println("Same price? {}", pen == Money{25, 50});
}
Output
Total: Rs. 410.05
pen < book? true
Same price? true

A few design rules are visible here:

  • operator+= changes the object and returns *this by reference, just like += on an int.
  • operator+ and operator* do not change either operand; they are const and return a new object. Writing + in terms of += avoids duplicate logic.
  • The {:02} format pads paise to two digits, so 5 paise prints as 05.

Comparisons with the spaceship operator

Before C++20, a class that wanted all comparisons had to write six functions: ==, !=, <, >, <= and >=. C++20 added the three-way comparison operator <=>, nicknamed the spaceship operator. Declaring it as = default makes the compiler compare the members in order, and from that one line you get all six operators. A defaulted <=> also gives you a defaulted ==.

a <=> b itself returns an ordering value that says "less", "equal" or "greater"; you rarely use it directly. Include <compare> when you use <=>.

If you need a custom order, write the body yourself, for example comparing students by marks and then by name:

C++
#include <compare>
#include <print>
#include <string>

struct Result {
    std::string name;
    int marks;

    auto operator<=>(const Result& other) const {
        if (auto cmp = marks <=> other.marks; cmp != 0) {
            return cmp;                     // marks decide first
        }
        return name <=> other.name;         // then alphabetical
    }
    bool operator==(const Result&) const = default;
};

int main() {
    Result a{"Rohan", 88};
    Result b{"Isha", 88};
    Result c{"Farhan", 92};
    std::println("{} {} {}", a < c, b < a, a == b);
}
Output
true true false

Rohan (88) is below Farhan (92). Rohan and Isha tie on marks, so the names decide, and "Isha" sorts before "Rohan". A hand-written <=> does not create == for you, so it is defaulted separately.

Printing your type: operator<< and std::formatter

To print with std::cout, write a free function operator<< that takes the stream by reference and returns it, so calls can be chained. To print with std::format and std::println, specialise std::formatter for your type. The simplest way is to inherit the string formatter and hand it your text.

C++
#include <format>
#include <iostream>
#include <print>
#include <string>

// ... the Money class from the first example ...

std::ostream& operator<<(std::ostream& out, const Money& m) {
    return out << std::format("Rs. {}.{:02}", m.rupees(), m.paise());
}

template <>
struct std::formatter<Money> : std::formatter<std::string> {
    auto format(const Money& m, std::format_context& ctx) const {
        auto text = std::format("Rs. {}.{:02}", m.rupees(), m.paise());
        return std::formatter<std::string>::format(text, ctx);
    }
};

int main() {
    Money fee{1299, 0};
    std::cout << "Course fee: " << fee << '\n';
    std::println("Course fee: {}", fee);
    std::println("[{:>14}]", fee);        // width works too
}
Output
Course fee: Rs. 1299.00
Course fee: Rs. 1299.00
[   Rs. 1299.00]

Because the formatter inherits from std::formatter<std::string>, standard options such as width and alignment work for free. operator<< must be a free function, not a member, because the left operand is the stream, not your object.

Subscript and function call operators

operator[] lets an object be indexed like an array. Provide a const version for reading from const objects and a non-const version that returns a reference for writing. operator() makes an object callable like a function; such objects are called function objects or functors.

C++
#include <array>
#include <print>
#include <string>

class Timetable {
public:
    std::string& operator[](int period) { return periods_.at(period); }
    const std::string& operator[](int period) const { return periods_.at(period); }

private:
    std::array<std::string, 4> periods_ = {"Maths", "Physics", "Hindi", "Computer"};
};

struct AboveMark {
    int limit;
    bool operator()(int marks) const { return marks > limit; }
};

int main() {
    Timetable monday;
    monday[2] = "English";                   // write through the reference
    std::println("{} then {}", monday[1], monday[2]);

    AboveMark distinction{75};
    std::println("{} {}", distinction(80), distinction(60));
}
Output
Physics then English
true false

Function objects are the ancestors of lambdas, which you will meet in Lambda Expressions.

Common mistakes

  • Surprising meanings. Overload an operator only when its meaning is obvious. + on Money is clear; + that prints a report is not.
  • Changing an operand in +. a + b must leave both unchanged; only compound operators like += modify the left side.
  • Returning a copy from += or []. Return *this by reference from +=, and a reference from [] so writes reach the object.
  • Making operator<< a member. It must be a free function taking std::ostream& first.
  • Overloading &&, || or the comma. The overloads lose short-circuit behaviour; avoid them.

Key points

  • An overloaded operator is a function named operator+, operator== and so on.
  • Implement + using +=; += returns *this by reference, + returns a new object.
  • auto operator<=>(const T&) const = default; (C++20) gives all six comparisons.
  • Print with std::cout through a free operator<<, and with std::println by specialising std::formatter.
  • operator[] should return a reference (plus a const overload); operator() creates function objects.

Exercise

Create a struct Distance that stores whole metres. Give it operator+, operator+=, a defaulted <=>, and a std::formatter that prints values of 1000 m or more as kilometres with two decimals (3.25 km) and smaller values as metres (750 m). Add 2500 m and 750 m, and print the total, the smaller distance and whether the first is greater than the second.

Show solution
C++
#include <compare>
#include <format>
#include <print>
#include <string>

struct Distance {
    long long metres = 0;

    Distance& operator+=(const Distance& other) {
        metres += other.metres;
        return *this;
    }
    Distance operator+(const Distance& other) const {
        Distance result = *this;
        result += other;
        return result;
    }
    auto operator<=>(const Distance&) const = default;
};

template <>
struct std::formatter<Distance> : std::formatter<std::string> {
    auto format(const Distance& d, std::format_context& ctx) const {
        std::string text = d.metres >= 1000
            ? std::format("{:.2f} km", d.metres / 1000.0)
            : std::format("{} m", d.metres);
        return std::formatter<std::string>::format(text, ctx);
    }
};

int main() {
    Distance walk{2500};
    Distance jog{750};
    std::println("Total: {}", walk + jog);
    std::println("Smaller: {}", jog < walk ? jog : walk);
    std::println("walk > jog? {}", walk > jog);
}
Output
Total: 3.25 km
Smaller: 750 m
walk > jog? true

The formatter decides how to show the value, so every std::println call prints distances consistently. > works because the defaulted <=> provides all comparisons.

Quick Quiz

1. What should operator+= return?

2. What does auto operator<=>(const T&) const = default; give you in C++20?

3. Why must operator<< for std::ostream be a free function?

4. How do you make std::println("{}", obj) work for your own type?

Learn C++ with a mentor

Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call