Operator Overloading
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.
#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});
}Total: Rs. 410.05 pen < book? true Same price? true
A few design rules are visible here:
operator+=changes the object and returns*thisby reference, just like+=on anint.operator+andoperator*do not change either operand; they areconstand return a new object. Writing+in terms of+=avoids duplicate logic.- The
{:02}format pads paise to two digits, so 5 paise prints as05.
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:
#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);
}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.
#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
}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.
#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));
}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.
+onMoneyis clear;+that prints a report is not. - Changing an operand in
+.a + bmust leave both unchanged; only compound operators like+=modify the left side. - Returning a copy from
+=or[]. Return*thisby reference from+=, and a reference from[]so writes reach the object. - Making
operator<<a member. It must be a free function takingstd::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*thisby reference,+returns a new object. auto operator<=>(const T&) const = default;(C++20) gives all six comparisons.- Print with
std::coutthrough a freeoperator<<, and withstd::printlnby specialisingstd::formatter. operator[]should return a reference (plus aconstoverload);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
#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);
}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?
Returning *this by reference matches built-in += and allows chaining.
2. What does auto operator<=>(const T&) const = default; give you in C++20?
A defaulted <=> provides <, >, <= and >=, and also a defaulted == (and so !=).
3. Why must operator<< for std::ostream be a free function?
std::cout << obj calls operator<<(std::cout, obj); your class is the right operand.
4. How do you make std::println("{}", obj) work for your own type?
std::format and std::println find the format logic in std::formatter<YourType>.
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
