Skip to content
VLSI Mentor

C++ Part I · Module 15 · Operator Overloading

When Not to Overload an Operator

Six chapters have been about getting overloads right, and every one of them eventually admitted the same thing: the mechanism has no opinion. The compiler will accept any overload whose syntax is valid, and the characteristic failure of this whole feature is not a diagnostic but a reader forming a confident, wrong belief about what an expression does. This chapter is that half of the subject on its own, because it is the half the mechanics cannot reach. It starts with the three operators that are not merely ill-advised but actively destructive to overload, each of which silently discards a guarantee the language gives you for free — measured, an overloaded && evaluated an operand that the built-in version provably does not. Then the single most common accident in this area, which is not an operator at all but a conversion left non-explicit: measured here letting two file handles be added together, producing 2, compiling clean at every warning level this compiler has, including the one that warns about include paths. Then a genuine surprise, which is what happens when you add a second conversion to the same type. And then the general shape of the mistake, which is reaching for notation because it is available rather than because a reader would understand it — along with the alternative that is almost always better, and an honest account of the small number of cases where notation really does win.

Intermediate18 min readC++Part IFundamentalsOperators

Part I · Module 15 · Chapter 15.7 · When Not to Overload an Operator

1. The Half the Mechanics Cannot Cover

The frozen brief for this module states the problem exactly: the failure mode here is not a compile error — it is code that reads as arithmetic and does something else.

Everything the previous six chapters could check, the compiler checks. Placement is an error (Chapter 15.2 §6). A wrong return type is an error (Chapter 15.6 §7) or a silent wrong answer with a known shape (Chapter 15.3 §6). But whether an overload should exist is not a property of the code, and no tool can hold an opinion about it.

So this chapter is a catalogue rather than a technique, and it has three parts: overloads that are mechanically destructive (§2–§4), the conversion mistake that accounts for most real damage (§5–§7), and the judgement that covers everything else (§8–§11).

2. The Three That Lose a Guarantee

&&, || and , are different from every other overloadable operator. The built-in versions carry evaluation guarantees, and an overload is a function call, so the guarantees do not survive.

OperatorWhat the built-in guaranteesAfter overloading
&&right operand not evaluated if left is falseboth evaluated
||right operand not evaluated if left is trueboth evaluated
,left evaluated fully before right; result is the right operandorder unspecified; result is whatever you return

These are the only operators where overloading removes something. For + you are adding a meaning that did not exist; for && you are replacing a meaning that did.

3. && and ||, Measured

Chapter 4.5 §3 was emphatic that short-circuiting is a guarantee rather than an optimisation. On built-in types:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
B. built-in && short-circuits:
     result=0  right operand evaluated 0 time(s)

With an overloaded operator&& on a class type, where each operand comes from a function that announces itself:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    bool operator&&(const B& a, const B& b) { return a.v && b.v; }
    bool y = check(false, "LEFT") && check(true, "RIGHT");
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
C. OVERLOADED && on class type:
     evaluated LEFT
     evaluated RIGHT
     result=0  operands evaluated: 2

Both ran. And notice the result was still false, which is the dangerous part: the answer is correct, so nothing looks wrong. What broke is the guard pattern Chapter 4.5 §4 established —

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    if (p && p->valid())        // built-in: safe

— because with an overload, p->valid() is evaluated even when p is null. The idiom that exists specifically to avoid a crash now causes one.

There is no way to write a correct overload. A function cannot conditionally not-evaluate its argument; the argument is evaluated before the function is entered. So the rule is absolute: never overload && or ||.

4. The Comma Operator, Measured

The built-in comma has two properties — it sequences left before right, and it yields the right operand:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A. BUILT-IN comma: sequencing and result
     left ran at step 1, right at step 2  -> left-to-right GUARANTEED
     value of (lhs, rhs) = 20  -> the RIGHT operand

Overloaded, it is a function call:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
B. OVERLOADED comma: an ordinary function call instead
     left ran at step 1, right at step 2
     value of (lhs, rhs).id = 1  -> whatever the overload returns (here the LEFT)
     the guaranteed left-to-right sequencing is GONE: argument order is
     unspecified, and this compiler happened to choose left-then-right

Two losses. The result is now whatever you return — here the left operand, where the built-in yields the right. And the sequencing guarantee is gone: function arguments are indeterminately sequenced with unspecified order, so although this compiler happened to run left-then-right, nothing requires it to.

That second point is worth stating carefully, because the measurement looks like nothing changed. It did: before, the order was guaranteed; after, it is a coincidence. Chapter 14.4 §3 made the same distinction about argument evaluation — the form is not reliably broken, it is permitted to break, which is worse because it passes testing.

Never overload ,. It also has essentially no use case; the built-in comma is itself a rarely-justified operator.

5. The Conversion That Permits Nonsense

Now the mistake that does the most real damage, and it is not an arithmetic overload at all. A conversion operator left non-explicit:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct Handle {
        int fd = 3;
        operator bool() const { return fd >= 0; }      // NOT explicit
    };

The intent is if (h). What you also get:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A. non-explicit operator bool permits nonsense:
     a + b   = 2   <-- two handles ADDED (via bool->int)
     a < 5   = 1   <-- a handle compared to an int
     a * 7   = 7
B. and the intended use also works:
     if (a)  -> true

a + b is 2. Two file handles, added together, producing the integer two — because operator bool lets each one become a bool, and bool promotes to int, and int + int is addition. Nothing in the class invites this and nothing prevents it.

With explicit, the intended use survives and the nonsense does not:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct Handle2 { int fd = 3; explicit operator bool() const { return fd >= 0; } };
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
C. explicit operator bool: intended use still works:
     if (c)  -> true

D. with explicit, the nonsense is rejected:
     c + d  -> error: invalid operands to binary expression ('Handle2' and 'Handle2')
     c < 5  -> error: invalid operands to binary expression ('Handle2' and 'int')
     c * 7  -> error: invalid operands to binary expression ('Handle2' and 'int')

if (c) still compiles because a conditional is a contextual conversion to bool, which explicit permits — that is the exact boundary Chapter 12.5 §4 drew for constructors, and it applies to conversion operators too.

Every operator bool should be explicit. There is no case where the implicit version is what you wanted.

Chapter 12.5 established this for one-argument constructors; conversion operators are the other direction of the same hazard, and they are easier to get wrong because the keyword looks redundant on something that returns bool.

6. And Nothing Warns

The warning audit on (a + b) + (a * 7) for the non-explicit Handle:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-Wall -Wextra -Wpedantic : exit 0, 0 warnings
-Weverything             : exit 0, 2 warnings

I read the two. They are -Wpoison-system-directories (about an include path) and -Wc++98-compat (about a default member initializer). Neither concerns adding two handles together.

So: no warning at any level in this compiler flags arithmetic on a type whose only intended use was if (h). One precise qualification — in the earlier run, a < 5 did draw -Wtautological-constant-out-of-range-compare, because comparing a bool against 5 is always true. But that warning is about the comparison's constant, not about the conversion, and a + b drew nothing at all.

This is the fifth time in two modules that counting grep matches in a warning sweep would have given me the wrong answer. Chapter 15.4 §4 reported the same thing for an incomplete ==.

7. The Twist: Two Conversions Collide

A genuine surprise, and a useful one. Add a second conversion to the same type:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct Handle {
        int fd = 3;
        operator bool() const { return fd >= 0; }
        operator int()  const { return fd; }       // a second conversion path
    };
    return h + 1;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: use of overloaded operator '+' is ambiguous (with operand types 'Handle' and 'int')
note: candidate function
note: candidate function

Now it is a hard error. With one conversion the nonsense compiled silently; with two, the compiler cannot choose a path and says so.

The lesson is not "add a second conversion" — that would be absurd. It is about the shape of the danger: a single implicit conversion is the dangerous number, because it is enough to make unintended expressions valid and not enough to create ambiguity. A type with one non-explicit conversion is maximally permissive and maximally silent.

8. The Shape of the Mistake

Underneath every entry in §9's catalogue is one pattern:

Reaching for notation because it is available, rather than because a reader would understand it.

The symptoms are recognisable. The call site gets shorter. The operator was chosen for its shape — * looks like joining, % looks like a separator, - looks like removal — rather than for a meaning readers already hold. And the justification, if pressed, is "it's documented" or "it's obvious once you know", both of which concede Chapter 15.1 §8's test: the question is whether a reader guesses right, not whether they can find out.

The giveaway question: if I showed this expression alone, with no type names, would anyone know what it did?

9. The Catalogue

OverloadWhy it is wrong
&&, ||§3 — short-circuiting lost, measured
,§4 — sequencing and result both lost
non-explicit operator bool§5 — arithmetic on your type compiles silently
any non-explicit conversion§7 — one is the dangerous number
+ meaning "append to a container"a reader expects a new value, not mutation; and + on vector was deliberately not provided
* meaning "join paths"no shared reading; / has a convention in some libraries and even that is contested
- meaning "remove an element"asymmetric with +, and removal is not arithmetic
% meaning "format"borrowed from other languages; not a C++ convention
~ meaning "destroy" or "reset"~ means bitwise complement
operator() on a non-callable conceptmakes an object look like a function it is not
[] with side effects beyond lookupreaders assume subscript is cheap and pure
== that is not an equivalenceChapter 15.4 §5 — breaks algorithms, measured
< that is not a strict weak orderingChapter 15.4 §6 — heap-buffer-overflow, measured
++ on something with no successorincrement implies a sequence

Two rows deserve emphasis because they are the ones people defend. + for container append is tempting and wrong: Chapter 15.1 §9 noted that std::vector has no + precisely because there is no single obvious meaning, and a + that mutates its left operand contradicts Chapter 15.3 §5's measured behaviour, where a + b left both operands alone. And % for formatting is a direct import from Python; it reads as remainder to a C++ audience.

10. The Alternative

Almost always: a named function.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    // instead of   path * "sub"           ->   path.join("sub")
    // instead of   list - item            ->   list.remove(item)
    // instead of   buf % args             ->   buf.format(args)
    // instead of   if (h)  via implicit   ->   if (h.isOpen())
    // instead of   a && b overloaded      ->   bothValid(a, b)

A named function has four properties an operator does not:

It says what it does, at every call site, in the reader's language rather than in symbols.

It can be searched for. grep join finds every use; grep '\*' does not.

It takes any number of arguments, in any order, with defaults — the grammar is not fixed (Chapter 15.1 §5).

And it can be overloaded without surprise, because nobody arrives with a prior belief about what join means on your type.

The cost is a few more characters, and Chapter 15.1 §4 measured that it is otherwise identical — byte-identical codegen, so the choice is purely about reading.

explicit is the named-function answer for conversions: explicit operator bool() plus a named isOpen() gives you both the idiom and the clarity, and many types are better off with only the named one.

11. When Notation Genuinely Wins

The chapter would be dishonest if it only said no. Four cases where an operator is clearly right:

Arithmetic on a type that is a number. Money, Duration, Complex, Matrix, BitVector. The reader's prior belief is correct and the notation carries real meaning — (a + b) * c is unreadable as multiply(add(a, b), c).

Comparison on a type with a natural order. Version, Timestamp, Address. And it is required if the type goes in an ordered container.

Subscript on something indexable and call on something that genuinely is a function — Chapter 15.6's operators, where the notation is the point.

And stream insertion, which Chapter 15.5 §4 argued is a grandfathered exception: it fails the guessing test and survives on a universal convention. The important half of that argument is that the exception is not transferable — you cannot cite << as precedent for a new overload, because << is readable only because it is already everywhere.

A useful final filter, which the standard library passes consistently: does an existing, widely-used type overload this operator for this meaning? If yes, the convention exists and you are joining it. If no, you are inventing one, and inventing conventions is how the entries in §9 got written.

12. What to Actually Do

Never overload &&, || or , — measured, each loses a guarantee you cannot restore.

Make every conversion operator explicit, especially operator bool — if (h) still works.

Prefer a named function whenever the test's answer is anything but a confident yes.

Overload arithmetic only on types that are numbers, and comparison only on types with a natural order.

Check whether a widely-used type already overloads this operator for this meaning. If not, you are inventing a convention.

Do not cite << as a precedent — it is grandfathered, not transferable.

Provide the named function as well as the operator where both make sense.

And read warning-sweep output rather than counting it — nothing here warns about any of this.

13. Mistakes Worth Making Once

Overloading &&. Measured: both operands evaluated, breaking the p && p->valid() guard.

Overloading ,. Measured: the result becomes whatever you return, and the sequencing guarantee is gone even when the order looks unchanged.

A non-explicit operator bool. Measured: a + b = 2 for two file handles, with no warning at any level.

Thinking explicit breaks if (h). It does not — a condition is a contextual conversion.

Adding a second conversion to "disambiguate". Measured: you get use of overloaded operator '+' is ambiguous, which is an improvement but not a design.

+ to append to a container. It mutates where + must not, and std::vector deliberately lacks it.

% for formatting. A Python idiom with no C++ convention behind it.

Choosing an operator for its shape. * looking like a join is not a meaning.

Answering the test with "it's documented". The test is about guessing.

Assuming a tool will catch a bad overload. None will — that is the module's whole difficulty.

14. Interview Relevance

"Which operators should you never overload?" &&, || and , — because the built-ins carry evaluation guarantees that a function call cannot preserve. The strong answer gives the measurement: an overloaded && evaluates both operands.

"Why is that worse than just being unusual?" It breaks the guard idiom p && p->valid(), which exists specifically to avoid dereferencing null.

"Should operator bool be explicit?" Always. Measured: without it, two handles can be added together producing 2, with no warning. if (h) still works because conditions use a contextual conversion.

"What is wrong with one implicit conversion?" It is enough to make unintended expressions valid and not enough to create ambiguity — measured, a second conversion turns the same expression into a hard error.

"When is overloading an operator the right call?" When the type is a number, has a natural order, is indexable, or is callable — and when a competent reader would guess the meaning without looking.

"How do you decide in a borderline case?" Named function. The failure modes are asymmetric: verbose-and-clear versus concise-and-misleading, with no tool to catch the second.

15. What You Should Be Able to Say Now

  1. The failure mode of this feature is not a diagnostic — it is a reader believing something wrong.
  2. &&, || and , are the only operators where overloading removes a guarantee.
  3. Measured: built-in && evaluated its right operand 0 times; overloaded, both were evaluated.
  4. That breaks p && p->valid() — the idiom that exists to prevent a null dereference.
  5. No correct overload of && is possible; arguments are evaluated before the function runs.
  6. Measured: the built-in comma guaranteed left-to-right and yielded the right operand (20).
  7. Measured: overloaded, it yielded whatever the function returns, and the sequencing guarantee is gone even though this compiler happened to keep the order.
  8. Measured: a non-explicit operator bool made a + b = 2, a < 5 = 1, a * 7 = 7 for file handles.
  9. Measured: explicit rejected all three while if (c) still worked — a condition is a contextual conversion (Chapter 12.5 §4).
  10. Measured: 0 warnings at -Wall -Wextra -Wpedantic and nothing relevant at -Weverything — I read the two matches.
  11. Measured: a second conversion makes the same expression ambiguous — so one implicit conversion is the dangerous number.
  12. The shape of the mistake is reaching for notation because it is available.
  13. The catalogue has fourteen entries, and + for append and % for formatting are the two most often defended.
  14. The alternative is a named function — it says what it does, is greppable, has a free grammar, and carries no prior belief.
  15. Notation genuinely wins for numbers, naturally ordered types, indexable types and callables — and for <<, which is grandfathered and not transferable.
  16. The useful filter: does a widely-used type already overload this operator for this meaning?
  17. The outcomes are asymmetric — verbose-and-clear versus concise-and-misleading — which settles every borderline case.

16. Exercises

Exercise 1 — the three absolutes. Name them, say what each loses, and give the measurement for &&.

Exercise 2 — the broken guard. Explain what p && p->valid() does with an overloaded && and why no overload can fix it.

Exercise 3 — predict the nonsense. For a Socket with a non-explicit operator bool, say what s1 + s2, s1 < 3 and s1 * 2 produce, and which draws a warning.

Exercise 4 — fix it. Add explicit and say which of the four expressions still compiles and why.

Exercise 5 — the second conversion. Say what adding operator int() does to h + 1, and what that tells you about how many implicit conversions are safe.

Exercise 6 — judge ten. Pass or fail with a reason: + on Duration; + on Playlist meaning append; / on Path meaning join; == on Transaction; < on Version; % on Template meaning substitute; [] on RegisterFile; () on Predicate; ~ on Connection meaning close; ++ on Semaphore.

Exercise 7 — rewrite as functions. For each of your fails above, give a named function and say what the call site gains.

Exercise 8 — the filter. Apply §11's final filter to three operators you were considering, and say what the answer tells you in each case.

Exercise 9 — the grandfathered case. Explain why << for output is acceptable and why that argument cannot be reused.

Exercise 10 — the asymmetry. State the two failure modes of a borderline decision and explain why they are not equally bad.

17. Summary

  • The failure mode is a reader's wrong belief, which no tool can detect — so this half of the subject is editorial.
  • Never &&, || or ,. Measured: built-in && evaluated its right operand 0 times, overloaded evaluated both, breaking the null-guard idiom. The comma loses its result and its sequencing guarantee.
  • Make every conversion explicit. Measured: without it, two file handles added to 2, with 0 warnings at every level — I read the -Weverything output to be sure.
  • Measured: a second conversion makes it ambiguous — one implicit conversion is the dangerous number.
  • The mistake is reaching for notation because it is available, and the giveaway is choosing an operator for its shape.
  • The default is a named function — it says what it does, is greppable, and has a free grammar at identical cost.
  • Notation wins for numbers, ordered types, indexable types and callables; << is grandfathered and not a precedent.
  • The outcomes are asymmetric, which decides every borderline case.

That completes Module 15, and with it the long run of modules about making a single type behave well. Module 11 gave a class an invariant, Module 12 gave it reliable construction and destruction, Module 13 made it copyable and movable on purpose, Module 14 made its ownership explicit in its interface, and this module gave it the notation a reader already understands. A type built with all five is self-contained and correct, and for a great deal of engineering work that is the whole answer.

What none of it can express is a relationship between types — that a WriteTransaction is a kind of Transaction and should be usable anywhere one is expected, that a family of bus protocols shares a common interface while differing in detail, or that a reference model and a fast approximate model are interchangeable behind one name. Module 16 is Inheritance: eight chapters on deriving one class from another, what a derivation actually asserts rather than what it lets you reuse, what is genuinely shared and what is merely visible, the access rules that decide which, the construction and destruction order that becomes subtle the moment a base class is involved, and the rule that decides most of these questions correctly — that public inheritance is a promise about substitutability, so a derived class which cannot keep that promise should have been a member instead. The mechanism that makes a hierarchy actually do anything at run time — virtual functions, dispatch, and the famous missing-destructor leak — is Module 17's subject and is deliberately held back, because almost every inheritance mistake is made before any virtual function is written.

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.