6. Operator Overloading¶
What & why¶
Operator overloading lets your types use natural syntax: a + b for vectors, p1 == p2 for points, std::cout << money. Done well, your types feel built in and work with the standard library (sorting, maps, printing). Done badly, + does something surprising. The rule: only overload an operator when its meaning is obvious.
A running example: Vec2¶
#include <cmath>
#include <compare>
#include <iostream>
struct Vec2 {
double x{};
double y{};
// Compound assignment: member, modifies *this, returns *this by reference
Vec2& operator+=(const Vec2& o) { x += o.x; y += o.y; return *this; }
Vec2& operator-=(const Vec2& o) { x -= o.x; y -= o.y; return *this; }
Vec2& operator*=(double s) { x *= s; y *= s; return *this; }
// Unary minus
Vec2 operator-() const { return {-x, -y}; }
// Equality (C++20: defaulted, compares members in order)
bool operator==(const Vec2&) const = default;
double length() const { return std::hypot(x, y); }
};
// Binary arithmetic: free functions, implemented in terms of the compound versions
inline Vec2 operator+(Vec2 a, const Vec2& b) { return a += b; }
inline Vec2 operator-(Vec2 a, const Vec2& b) { return a -= b; }
inline Vec2 operator*(Vec2 v, double s) { return v *= s; }
inline Vec2 operator*(double s, Vec2 v) { return v *= s; } // so 2.0 * v works
// Stream output
inline std::ostream& operator<<(std::ostream& os, const Vec2& v) {
return os << '(' << v.x << ", " << v.y << ')';
}
Vec2 a{1, 2}, b{3, 4};
std::cout << a + b << '\n'; // (4, 6)
std::cout << 2.0 * a << '\n'; // (2, 4)
std::cout << (a == b) << '\n'; // 0
a += b;
std::cout << -a << '\n'; // (-4, -6)
Why free functions for binary operators?¶
A member operator+ only allows implicit conversions on the right operand. A free function treats both sides symmetrically, so 2.0 * v and v * 2.0 both work.
Taking the left operand by value (Vec2 a) gives you a copy to modify and return — a neat idiom.
Comparisons in C++20: == and <=>¶
Before C++20 you wrote six comparison operators. Now:
struct Version {
int major{}, minor{}, patch{};
auto operator<=>(const Version&) const = default; // generates <, <=, >, >=
bool operator==(const Version&) const = default; // generates ==, !=
};
Version{1, 2, 3} < Version{1, 10, 0}; // true — compares members in declaration order
<=> ("spaceship") returns an ordering: std::strong_ordering, std::weak_ordering, or std::partial_ordering (for floating point, where NaN is unordered).
(Actually, defaulting <=> also implicitly defaults ==, but writing both is clearer.)
A custom ordering — case-insensitive names:
#include <algorithm>
#include <cctype>
struct Name {
std::string value;
std::weak_ordering operator<=>(const Name& o) const {
auto lower = [](unsigned char c) { return std::tolower(c); };
return std::lexicographical_compare_three_way(
value.begin(), value.end(), o.value.begin(), o.value.end(),
[&](char a, char b) { return lower(a) <=> lower(b); });
}
bool operator==(const Name& o) const { return (*this <=> o) == 0; }
};
Once a type has <, it works with std::sort, std::map, and std::set with no extra code.
Stream operators¶
std::ostream& operator<<(std::ostream& os, const T& value); // output
std::istream& operator>>(std::istream& is, T& value); // input
Always return the stream so calls chain. If they need private members, declare them as friend inside the class:
class Money {
public:
explicit Money(long long cents) : cents_{cents} {}
friend std::ostream& operator<<(std::ostream& os, const Money& m) {
return os << '$' << m.cents_ / 100 << '.'
<< (m.cents_ % 100 < 10 ? "0" : "") << m.cents_ % 100;
}
private:
long long cents_;
};
std::format support¶
To make std::format("{}", money) work, specialize std::formatter:
#include <format>
template <>
struct std::formatter<Vec2> : std::formatter<std::string> {
auto format(const Vec2& v, std::format_context& ctx) const {
return std::formatter<std::string>::format(std::format("({}, {})", v.x, v.y), ctx);
}
};
std::cout << std::format("pos = {}\n", Vec2{1, 2});
Subscript []¶
class Grid {
public:
Grid(std::size_t w, std::size_t h) : w_{w}, cells_(w * h) {}
int& operator[](std::size_t row, std::size_t col) { // C++23 multi-arg
return cells_[row * w_ + col];
}
// C++20-compatible alternative: int& at(std::size_t row, std::size_t col);
private:
std::size_t w_;
std::vector<int> cells_;
};
Provide both a const and a non-const overload when appropriate:
int& operator[](std::size_t i) { return data_[i]; }
const int& operator[](std::size_t i) const { return data_[i]; }
Function call (): function objects¶
A class with operator() can be called like a function, and can carry state:
struct Multiplier {
int factor;
int operator()(int x) const { return x * factor; }
};
Multiplier triple{3};
triple(5); // 15
std::ranges::transform(v, v.begin(), Multiplier{2}); // double every element
Lambdas (lesson 9) are just a short way to write these.
Conversion operators¶
class Percentage {
public:
explicit operator double() const { return value_ / 100.0; }
explicit operator bool() const { return value_ != 0; } // for `if (p)`
private:
double value_{};
};
Make them explicit to avoid surprising implicit conversions. An explicit operator bool still works in if, while, &&, !.
Hashing for unordered_map¶
To use your type as an unordered_map key, provide == and a std::hash specialization:
template <>
struct std::hash<Vec2> {
std::size_t operator()(const Vec2& v) const noexcept {
std::size_t h1 = std::hash<double>{}(v.x);
std::size_t h2 = std::hash<double>{}(v.y);
return h1 ^ (h2 + 0x9e3779b97f4a7c15ULL + (h1 << 6) + (h1 >> 2));
}
};
std::unordered_map<Vec2, std::string> labels;
Rules of thumb¶
| Operator | Member or free? | Returns |
|---|---|---|
=, [], (), ->, compound += etc. |
member | T& (for =/+=), element ref for [] |
binary + - * / |
free (often via +=) |
T by value |
==, <=> |
member, often = default |
bool / ordering |
<<, >> for streams |
free (maybe friend) |
the stream by reference |
- Keep semantics unsurprising:
+shouldn't modify its operands. - Keep related operators consistent: if
a + bexists,a += bshould too. - Don't overload
&&,||, or,— you lose short-circuiting and evaluation order.
Common mistakes¶
- Returning by value from
+=or by reference from+. - Member
operator+that breaks2 * v. - Forgetting
conston comparison operators — then they don't work onconstobjects or instd::sort. - Inconsistent
==and<=>. - An
operator<that isn't a strict weak ordering —std::sortbecomes undefined behavior (e.g.<=instead of<). - Implicit conversion operators that cause ambiguous overloads.
Exercises¶
- Complete
Vec2with/,/=,dot,normalized(),operator>>, and astd::formatter. - Write a
Fractionclass (from lesson 4) with+ - * /,<=>,==, and<<. Sort a vector of fractions. - Write a
Matrix2x2with*for matrix multiplication andoperator[](row, col)(orat(row, col)if your compiler lacks C++23). - Make a
Date {int y, m, d;}usable as a key in bothstd::map(via<=>) andstd::unordered_map(viastd::hash). - Write a function object
CountingLessthat counts how many comparisonsstd::sortmakes on 1,000 random ints. (Careful:std::sortcopies the comparator — use a reference or pointer to the counter.)
Recap¶
- Overload operators only when the meaning is obvious.
- Implement
+=as a member returning*this; implement+as a free function using it. - C++20:
= default==and<=>give you all six comparisons. operator<<returns the stream; specializestd::formatterforstd::format.operator()makes function objects;explicitconversion operators avoid surprises.- Provide
std::hash+==for unordered containers.