What you'll learn
Operators are functions with familiar syntax. Used well, they make a domain value read like built-in arithmetic or comparison; used poorly, they hide surprising work. You will implement a small numeric type, streams, comparisons, indexing, and deliberate conversions.
By the end, you'll be able to:
- Choose member or non-member operator forms
- Implement arithmetic, comparison, stream, and increment conventions
- Keep indexing and call operators within their natural meaning
- Control converting constructors and conversion operators with explicit
Operator design principles
Overloading changes how an operator works for a user-defined type; it cannot invent new syntax, change precedence, change arity, or redefine behavior for built-in operands alone. The result should preserve the operator's conventional meaning and complexity expectations.
- Use + to produce a value and += to modify the left operand
- Comparison should be stable, transitive, and consistent with equality
- Stream insertion writes to the supplied stream and returns that stream
- Do not make arithmetic trigger hidden I/O, networking, or unrelated mutation
Key idea
Arithmetic operators
Compound assignment is naturally a member because it modifies its left object. Binary arithmetic is often a non-member implemented in terms of compound assignment; this treats both operands more symmetrically and reuses the invariant-preserving logic.
#include <iostream>
class Distance {
public:
explicit Distance(double meters) : meters_{meters} {}
Distance& operator+=(Distance other) {
meters_ += other.meters_;
return *this;
}
double meters() const { return meters_; }
private:
double meters_{};
};
Distance operator+(Distance left, Distance right) {
left += right;
return left;
}
int main() {
Distance route{120.5};
route += Distance{29.5};
std::cout << (route + Distance{50.0}).meters() << '\n';
}Equality and three-way comparison
C++20 can generate coherent comparisons with a defaulted operator== andoperator<=>. Memberwise ordering is appropriate only when member declaration order matches the domain's ordering. Floating-point values may produce partial ordering because NaN is unordered.
#include <compare>
#include <iostream>
struct Version {
int major{};
int minor{};
int patch{};
bool operator==(const Version&) const = default;
auto operator<=>(const Version&) const = default;
};
int main() {
Version current{2, 4, 0};
Version required{2, 3, 5};
std::cout << std::boolalpha << (current >= required) << '\n';
}Watch out
Stream insertion and extraction
Stream operators are non-members because the stream is the left operand. Returning the stream enables chaining. Extraction should set failbit when input cannot produce a valid value and should avoid partially mutating the destination.
#include <iostream>
struct Point { double x{}; double y{}; };
std::ostream& operator<<(std::ostream& out, const Point& point) {
return out << '(' << point.x << ", " << point.y << ')';
}
std::istream& operator>>(std::istream& in, Point& point) {
double x{};
double y{};
if (in >> x >> y) point = Point{x, y};
return in;
}
int main() {
Point point{3.5, 7.0};
std::cout << "point: " << point << '\n';
}Subscript and call operators
operator[] makes an object behave like an indexed collection and often needs const and non-const overloads. operator() makes a function object whose state can parameterize repeated calls. Both should have clear preconditions and stable reference lifetimes.
| Operator | Conventional role |
|---|---|
object[index] | Element access |
object(args) | Callable behavior |
pointer->member | Pointer-like access; overload sparingly |
static_cast<bool>(object) | Valid/present/open state |
#include <iostream>
class Scale {
public:
explicit Scale(double factor) : factor_{factor} {}
double operator()(double value) const { return value * factor_; }
private:
double factor_;
};
int main() {
Scale kilometers_to_miles{0.621371};
std::cout << kilometers_to_miles(10.0) << '\n';
}Increment and conversions
Prefix increment modifies and returns the object by reference. Postfix receives a dummyint, copies the old value, performs prefix increment, then returns the old copy. Converting constructors and conversion operators should be explicit unless implicit conversion is safe, cheap, unsurprising, and semantically exact.
class TicketNumber {
public:
explicit TicketNumber(unsigned value) : value_{value} {}
TicketNumber& operator++() {
++value_;
return *this;
}
TicketNumber operator++(int) {
TicketNumber previous{*this};
++*this;
return previous;
}
explicit operator bool() const { return value_ != 0; }
private:
unsigned value_{};
};Tip
if (object) works with an explicit operator bool, avoiding unintended arithmetic conversions.Recap & quick check
Key takeaways
- Overloaded operators should preserve conventional meaning and expected cost.
- Implement binary arithmetic from compound assignment when that yields one source of truth.
- Defaulted C++20 comparisons are useful when memberwise order matches domain order.
- Stream operators return the supplied stream so expressions can chain.
- Make conversion paths explicit unless implicit conversion is exact and unsurprising.
Quick check
1. What should operator+= normally return?
2. Why is stream insertion usually a non-member?
3. What distinguishes postfix increment's declaration?
4. When is an implicit conversion most defensible?
Next: Module 13 — Inheritance, Polymorphism & Interfaces, where behavior varies behind stable runtime contracts.