C++ Part I · Module 15 · Operator Overloading
What Operator Overloading Is For
Module 14 spent eight chapters using operators on standard types without ever showing how a type acquires them. A unique_ptr was dereferenced with * and reached through with ->, a shared_ptr was tested with if, a vector was indexed with [], and a std::string was joined with +. None of those is built in to the language the way integer addition is; every one is a function somebody wrote, with a name the grammar already knows. That is the whole mechanism, and taking it literally is the most useful thing you can do with it: an operator is an ordinary function with unusual syntax, subject to the same overload resolution as any other function, compiling to the same instructions, and possessing an address you can take and call through. This chapter measures all three of those claims rather than asserting them. Then the three things the mechanism does not let you change, because each is a place where people expect flexibility that is not there — precedence, associativity, and evaluation order are fixed by the grammar before your function is ever selected, which means an overloaded && evaluates both of its operands and the guarantee Chapter 4.5 called a guarantee rather than an optimisation is simply gone. And then the part that matters more than any of the mechanics. The failure mode here is not a compile error. It is code that reads as arithmetic and does something else, and the only defence is a test you apply before writing the function rather than a rule the compiler can check.
Intermediate17 min readC++Part IFundamentalsOperators
Part I · Module 15 · Chapter 15.1 · What Operator Overloading Is For
1. An Operator Is a Function
struct Money { long cents; };
Money operator+(const Money& a, const Money& b) { return Money{a.cents + b.cents}; }That is a function definition. Its name is operator+, its parameters are two const Money&, its return type is Money, and the only unusual thing about it is that the grammar lets you call it by writing a + b.
Everything else follows from that sentence, so it is worth stating as the chapter's thesis:
operator+is a function with a name the compiler already knows. Writinga + bis calling it.
Chapter 4.2 established what + means for built-in types, and that meaning is fixed — you cannot change what int + int does. Overloading adds a new overload for operand types the language had no meaning for, which is Chapter 6.7's mechanism applied to a function whose name happens to be punctuation.
2. The Three Spellings
If it is a function, the function-call syntax should work too. Measured, with an ordinary function alongside for comparison:
Money operator+(const Money& a, const Money& b) { return Money{a.cents + b.cents}; }
Money added (const Money& a, const Money& b) { return Money{a.cents + b.cents}; }A. a + b = 425
operator+(a, b) = 425
added(a, b) = 425a + b and operator+(a, b) are the same call, written two ways. The second spelling is legal, occasionally useful when you need to bypass overload resolution deliberately, and mostly valuable as a reminder of what the first one is.
3. It Has an Address
A function has a type and an address. So does an operator:
Money (*viaOp)(const Money&, const Money&) = &operator+;
Money (*viaFn)(const Money&, const Money&) = &added;B. through a function pointer to operator+: 425
same address as &operator+ ? 1
&operator+ == &added ? 0 (different functions)The pointer types are identical — Money(*)(const Money&, const Money&) — because the two functions differ only in name. One can be assigned to a variable, passed to another function, stored in a table. There is nothing special about it.
And the mangled names make the point from the other direction:
operator+ -> __ZplRK5MoneyS1_
added -> __Z5addedRK5MoneyS1_pl is simply the Itanium ABI's spelling of "plus" where 5added is a length-prefixed identifier. At the level the linker works at, an operator is a function with a reserved name.
4. It Costs Nothing
long viaOperator(Money a, Money b) { return (a + b).cents; }
long viaFunction(Money a, Money b) { return added(a, b).cents; }At -O2:
__ZplRK5MoneyS1_ (4): ldr x8,[x0] | ldr x9,[x1] | add x0,x9,x8 | ret
__Z5addedRK5MoneyS1_ (4): ldr x8,[x0] | ldr x9,[x1] | add x0,x9,x8 | ret
__Z11viaOperator... (2): add x0,x1,x0 | ret
__Z11viaFunction... (2): add x0,x1,x0 | retByte-identical, both for the two function bodies and for the two call sites, which inlined to a single add. Chapter 1.1's claim about abstraction that costs nothing when used as intended holds here exactly: the notation is free.
So performance is never a reason to prefer added(a, b) to a + b, and never a reason to prefer the operator either. The choice is entirely about readability, which is why §8 is the part of this chapter that matters.
5. What You Cannot Change: Precedence and Associativity
Here is where expectations go wrong. Overloading supplies a meaning for an operator on your types. It does not supply a grammar.
Two types whose + and * record the order in which they are called:
T operator+(const T& a, const T& b) { log_[n_++] = '+'; return T{a.v + b.v}; }
T operator*(const T& a, const T& b) { log_[n_++] = '*'; return T{a.v * b.v}; }
T a{2}, b{3}, c{4};
T r = a + b * c;A. a + b * c with overloaded + and * :
call order: *+ result = 14
2 + (3*4) = 14, (2+3)*4 = 20 -> precedence is FIXED by the grammar* was called first, and the result is 14, not 20. The expression was parsed as a + (b * c) before overload resolution ran, because Chapter 4.9 §2's precedence table is part of the grammar and has nothing to do with which functions exist.
The same applies to associativity (Chapter 4.9 §4) and to arity: you cannot make + right-associative, you cannot give it three operands, and you cannot invent a new symbol. Chapter 4.9 §1's "two different questions" framing is the one to carry: the grammar decides what groups with what; your function only decides what the grouping means.
The practical consequence is a constraint on design. If your * does not bind tighter than your +, the notation will mislead, and you cannot fix it by overloading differently. You can only choose different operators or a named function.
6. What You Cannot Change: Short-Circuiting
This one is sharper, because something is silently taken away rather than merely fixed.
Chapter 4.5 §3 was emphatic that short-circuiting is a guarantee, not an optimisation — the right operand of && is not evaluated when the left is false, and you may rely on it. Measured on built-in types:
B. built-in && short-circuits:
result=0 right operand evaluated 0 time(s)Now the same shape with an overloaded && on a class type, where each operand is produced by a function that announces itself:
bool operator&&(const B& a, const B& b) { return a.v && b.v; }
bool y = check(false, "LEFT") && check(true, "RIGHT");C. OVERLOADED && on class type:
evaluated LEFT
evaluated RIGHT
result=0 operands evaluated: 2Both operands were evaluated. The result was still false, so nothing looked wrong — but check(true, "RIGHT") ran, and in real code that is a file opened, a null pointer dereferenced through, or an expensive query performed.
The reason is exactly §1's thesis. An overloaded && is a function call, and a function call evaluates its arguments. The special evaluation rule belongs to the built-in &&, not to the symbol. The same is true of || and of the comma operator, which Chapter 15.7 measures.
Overloading
&&,||or,silently removes the one property that made them worth having. They are on 15.7's list of operators not to overload for precisely this reason.
7. What You Cannot Overload at All
A short list, and it is a language rule rather than something to discover experimentally:
| Cannot be overloaded | Why it matters |
|---|---|
. (member access) | the meaning of naming a member is fixed |
.* (pointer-to-member access) | same |
:: (scope resolution) | resolved at compile time against names, not values |
?: (conditional) | its evaluation rule is the point of it |
sizeof, alignof | compile-time properties of a type |
typeid | likewise |
co_await (as a non-member) | — |
An honest note on how this was checked. I tried to produce informative diagnostics for these by writing operator., operator:: and so on. Every attempt produced the same unhelpful parse error — expected a type — because the compiler fails while reading the declaration and never reaches a rule about overloadability. So this table is quoted from the language rules, not measured, and the practical takeaway is small but real: if you try one of these, expect a confusing syntax error rather than a message explaining the restriction.
Two more rules worth knowing now, both measured in later chapters: =, [], () and -> must be members (Chapter 15.2 §6), and at least one operand of an overloaded operator must be a user-defined type — you cannot overload + for two ints.
8. The One Test Worth Applying
Everything above is mechanics, and the mechanics are easy. The hard part is that the mechanism has no opinion about whether your overload is a good idea, and the compiler cannot have one either.
The module's brief states the failure mode precisely: it is not a compile error, it is code that reads as arithmetic and does something else. A wrong overload compiles cleanly, passes review quickly because the call site is short, and misleads every reader afterwards.
So there is one test, and it is worth stating as a question you answer before writing the function:
Would a competent reader, seeing this expression and not having read your class, guess what it does?
Not could they work it out — they can always work it out by opening the header. Guess. Correctly, on the first try, without checking.
Three worked examples of the test:
a + b on a Money type. Pass. Every reader guesses "add the amounts", and that is what it does.
a + b on a Matrix type. Pass for element-wise addition, which is the universal convention. Fail for concatenation or for matrix multiplication, which a reader would expect from *.
a + b on a Transaction type. Fail. There is no shared idea of what adding two bus transactions means, so every reader has to look — and the shortness of + has bought nothing while costing clarity.
The test has a useful property: it is about your reader's prior knowledge, not about your type's internals. That is why it can be applied before any code exists, and why "but it's documented" is not an answer to it. Chapter 10.9 §6 made the general version of this argument for ownership — a comment is read once by whoever is curious, while notation is read every time.
9. Where the Standard Library Earns It
The library is the best available evidence that the test is the right one, because it passes everywhere it overloads and declines everywhere it would fail.
| Overload | Why it passes |
|---|---|
std::string + | joining text is the universal reading of + on strings |
std::vector [] | indexing is what brackets mean |
std::complex arithmetic | complex numbers are numbers; the notation is the mathematics |
std::unique_ptr * and -> | Chapter 14.2 §2 — the type is a pointer, so pointer notation reads correctly |
std::shared_ptr operator bool | "is it there" is the conventional reading of testing a pointer |
stream << | Chapter 15.5 — a deliberate convention, learned once and then universal |
And the declines are as instructive. std::vector has no + — there is no single obvious meaning for adding two vectors, so the library makes you say insert or std::transform. std::map has no + either. std::string has no * for repetition, which some languages do provide, because s * 3 is not universally read as repetition.
The one entry worth flagging as a genuine exception is stream <<, which fails a strict reading of the test: nothing about a left-shift suggests output. It survives because the convention is old, consistent, and learned exactly once — which Chapter 15.5 argues is a real but non-renewable justification. You do not get to spend it on a new overload.
10. What to Actually Do
Read a + b as operator+(a, b) — it is the same call, measured.
Expect no cost — byte-identical codegen, so choose on readability alone.
Do not expect to change the grammar. Precedence, associativity and arity are fixed before your function is selected.
Never overload &&, || or , — measured, the special evaluation rule is lost.
Apply §8's test before writing the function: would a competent reader guess right?
Follow the library's lead — it overloads where the convention is universal and declines where it is not.
And treat "it's documented" as a non-answer, because notation is read every time and a comment is read once.
11. Mistakes Worth Making Once
Thinking an operator is special. It is a function with a reserved name — __Zpl… at link time.
Expecting to change precedence. Measured: a + b * c gave 14, grouping as a + (b*c).
Overloading &&. Measured: both operands evaluated, and the result still looked right.
Trying to overload . or sizeof. You get expected a type, not an explanatory message.
Overloading + for two built-in types. At least one operand must be user-defined.
Choosing an operator for brevity. The call site gets shorter and every reader gets slower.
Assuming a documented overload is a clear one. §8's test is about guessing, not about looking it up.
Expecting a tool to catch a bad overload. There is none — this is the module's whole difficulty.
12. Interview Relevance
"What is operator overloading?" Defining a function whose name is an operator, so the operator's syntax calls it. The strong answer says function first and demonstrates that a + b and operator+(a, b) are the same call.
"Does it cost anything at runtime?" No — measured byte-identical to a named function, inlining to a single instruction at the call site.
"Can you change an operator's precedence?" No. The grammar parses the expression before overload resolution runs — measured as a + b * c producing 14.
"What happens if you overload &&?" It becomes a function call and short-circuiting is lost — both operands evaluated, measured. This is a strong discriminator; many candidates have never tried it.
"Which operators cannot be overloaded?" ., .*, ::, ?:, sizeof, alignof, typeid.
"When should you overload an operator?" When a competent reader would guess correctly without looking. The willingness to answer with a judgement test rather than a list of mechanics is what distinguishes here.
13. What You Should Be Able to Say Now
- An operator is an ordinary function with a name the grammar knows.
- Measured:
a + b,operator+(a, b)and a namedadded(a, b)all produced 425. - Measured:
&operator+has the same function-pointer type as&added, and comparing them gave 1 and 0 respectively — same function, different functions. - The mangled name is
__Zpl…— a function with a reserved name, at the level the linker sees. - Measured: byte-identical bodies (4 instructions each) and byte-identical call sites (2 each).
- So the choice between notation and a named function is purely about readability.
- Precedence and associativity are fixed by the grammar — measured,
a + b * ccalled*first and gave 14, not 20. - You cannot change arity or invent symbols either.
- Measured: built-in
&&evaluated its right operand 0 times; an overloaded&&evaluated both. - Short-circuiting is a guarantee of the built-in operator, not of the symbol (Chapter 4.5 §3).
.,.*,::,?:,sizeof,alignofandtypeidcannot be overloaded — and the diagnostic you get is an unhelpful parse error.=,[],()and->must be members, and at least one operand must be user-defined.- The test: would a competent reader guess what the expression does, without reading your class?
- The failure mode is neither a compile error nor a crash — it is a reader forming a wrong belief, which no tool can detect.
- The library passes the test where it overloads and declines where it would fail —
stringhas+,vectordoes not.
14. Exercises
Exercise 1 — the three spellings. For a Money type with a non-member operator+, write the same addition three ways and say which two are the same call.
Exercise 2 — the address. Declare a function pointer that can hold &operator+ for Money, and say why it has the same type as a pointer to a named function.
Exercise 3 — predict the grouping. With overloaded + and * on a type T holding 2, 3 and 4, give the value of a + b * c and the order in which the two functions are called.
Exercise 4 — the short-circuit. openIfNeeded(h) && validate(h) where both return a class type and operator&& is overloaded. Say what runs when the first returns false, and what the built-in version would have done.
Exercise 5 — apply the test. For each, pass or fail §8's test with a reason: + on a Duration; + on a Logger; [] on a RegisterFile; < on a Version; * on a FilePath meaning "join"; ~ on a BitVector meaning complement.
Exercise 6 — the library's declines. Say why std::vector has [] but not +, and why std::string has + but not *.
Exercise 7 — cost. A colleague says a + b is slower than add(a, b) because it "goes through the operator machinery". Give the measurement that answers this.
Exercise 8 — the non-overloadables. Name four operators that cannot be overloaded, and say what diagnostic to expect if you try.
15. Summary
- An operator is an ordinary function with a reserved name.
a + bisoperator+(a, b)— measured, both giving 425, with__Zpl…as the linker's spelling. - Measured: it has an address and a function-pointer type indistinguishable from a named function's.
- Measured: byte-identical codegen, 4-instruction bodies and 2-instruction call sites — the notation is free, so choose on readability alone.
- The grammar is not yours to change. Measured:
a + b * cgrouped asa + (b*c)and gave 14. - Measured: an overloaded
&&evaluated both operands where the built-in evaluated the right one 0 times — the guarantee is the built-in operator's, not the symbol's. .,.*,::,?:,sizeof,alignof,typeidcannot be overloaded, and the diagnostic is an unhelpful parse error.- The one test: would a competent reader guess correctly without looking?
- The failure mode is neither a compile error nor a crash, which is why judgement rather than tooling is the defence.
The mechanism is settled, and the next question is one §1 quietly sidestepped. The operator+ in this chapter was a free function taking both operands as parameters, but it could equally have been a member of Money, called as if the left operand were the object — and for some operators that is the only legal choice, while for others it is forbidden. The deciding consideration is not style. It is that a member function's left operand is always, necessarily, an object of the class, which means the conversions the compiler is willing to apply are not the same on both sides — so one placement makes 2 * v work and the other rejects it with a diagnostic worth seeing. The next chapter is that asymmetry, measured, along with the four operators the language requires to be members and the reason stream insertion cannot be one.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
