Ch17: Operator Overloading
1. What Overloading Buys, and What It Costs
An overloaded operator is a function whose name is punctuation. That is the entire
feature. a + b is exactly operator+(a, b), a[i] is
exactly a.operator[](i), and the compiler's job is nothing more than
substituting the second spelling for the first
(Ch17.1).
The buy is real: for a numeric type, a * b + c reads the way the mathematics
reads, and no reader has to decode a name. For a smart handle,
p->member lets a wrapper be used where a pointer was used.
The cost is also real, and it is not the cost you would guess. It is not the extra
function call — that is inlined. The cost is that punctuation carries no
information. + cannot tell the reader whether it concatenated two
sequences, allocated, copied ten megabytes, failed, or was associative. A named function
can. A named function can also be found by a text search, put in a debugger breakpoint
list, and called from a language that has no operator+.
2. The Rule This Chapter Argues For
Prefer a named function. Overload an operator only when the operator's meaning is the meaning the type already has, and when the operation cannot fail, cannot allocate, and cannot surprise.
The canonical example is sequence concatenation. For two strings,
a + b is a C++ habit inherited from 1998 that says nothing about what happens
to the bytes: it produces a new string, which means it copies every byte of
a and every byte of b, which means an allocation, which means
an error path. concat(a, b) says "produce one sequence out of two", and
a.append(b) says "extend a with b, no new value,
no copy of a". Three different operations, three different costs, and with
+ you cannot tell which one you got. Chapter
17.2 measures the difference:
the same 4,000-byte result costs 998,000 copied bytes with + and zero with
append.
Note what this rule is not. It is not "never overload operators" —
Ch17.3 shows a fixed-point
type where + and * are the right answer, and
Ch17.4 shows
== and <=> done properly. It is "do not use punctuation
to hide a decision the reader has to know about".
3. The Table, in One Screen
| Operator | Verdict | Reason |
|---|---|---|
+ - * / on a numeric value type |
yes | the type is arithmetic; no allocation, no failure, total (Ch17.3) |
==, <=> |
yes | if the type has one obvious equality and one obvious order (Ch17.4) |
=>>=-style compound assignment |
yes | it is the primitive; + is defined in terms of it |
[] |
yes, if it is cheap and cannot fail | container index; the moment it can insert or throw it lies (Ch17.7) |
*, -> |
yes, for handles | a wrapper that behaves like a pointer |
() |
yes, for callables | it is the contract every algorithm that takes a callable relies on; make it
static inline constexpr when the callable is stateless, and never use it
as a "do everything" entry point
(Ch17.7 §6) |
+ on containing types |
no | hides allocation, copying, and failure; use concat or
append (Ch17.2) |
, |
never | a function call cannot sequence its arguments, and sequencing is the only thing the comma operator is for (Ch17.8) |
& (unary, address-of) |
never | it is how C has named an object for fifty years, and the repair cost the standard
library std::addressof
(Ch17.8 §3) |
<<, >> for I/O |
never | shifting is not printing; iostreams are bad and harmful, which is why fast_io has
print and scan
(Ch17.8) |
operator T() |
only explicit operator bool |
invisible conversions at a distance; the implicit ones silently change what
== and + mean
(Ch17.6) |
++, -- |
yes, prefix first | the prefix form is the primitive; postfix is three lines and one copy (Ch17.5) |
operator new / operator delete |
never | Ch18.7 shows the whole bill |
4. Contents
- Ch17.1: Operators Are
Functions — what the compiler actually does, member vs non-member, what can
and cannot be overloaded, and the canonical
+=-then-+shape. - Ch17.2: Prefer a Named
Function:
concat, Not+— the measured+/concat/appendcomparison, and the four questions to ask before writing punctuation. - Ch17.3: Arithmetic on
Value Types — the case where
+is correct: fixed-point,noexcept,[[nodiscard]], symmetry, and no allocation. - Ch17.4: Equality and
Ordering —
= default,<=>, total vs partial order, and why a comparator is sometimes the named function you want. - Ch17.5: Increment and
Decrement — why the prefix form is the primitive, how to write
++iandi++so they cannot drift apart, what the postfix copy costs, and what[[nodiscard]]enforces. - Ch17.6: Conversion
Operators —
explicit operator boolas the only conversion worth having, two measured bugs from implicit conversions, and the named accessors that replace them. - Ch17.7: Subscript, Call, and
Dereference —
[],(),*,->, the insertingoperator[]trap, and proxy references. - Ch17.8: Operator
Overloadings That Should Not Exist — the two that have no defensible use
(
,and&, the second of which cost the standard librarystd::addressof), and the standard library's own<</>>, which took the shift operators for I/O. - Ch17.9: Rules and a Checklist — the whole chapter as decisions you can grep for in review.
5. What You Need Before This Chapter
- Functions and overloading (Chapter 6).
- References and
const(Ch2.9). - The five special members (Chapter 14), because an operator that returns a new value is a copy or move question.
- Contracts: every operator you write is a function, so it is
noexceptorthrows(Ch6.16).