Skip to content
VLSI Mentor

C++ Part I · Module 15 · Operator Overloading

Arithmetic and Compound Assignment Operators

Every arithmetic idea in C++ comes with two operators rather than one. There is + and there is +=, and a reader who has used any built-in type believes something about how they relate: that a += b leaves a holding what a = a + b would have given it. Nothing in the language enforces that belief for your type. You write two functions, the compiler checks each on its own, and if they drift apart the result is a type on which both expressions compile, neither warns, and they produce different answers — measured here as twelve thousand two hundred and seventy-three disagreements out of nineteen thousand four hundred and twenty-five pairs of values, from one plausible pair of operators where one of them applied a rule the other forgot. The convention that makes the drift impossible is to write only one of the two and derive the other, and the direction is the one people get backwards: the compound form carries the logic, and the binary form is built from it rather than the other way round. Then the return types, which is where this chapter stops being about convention and starts being about memory. Three choices look interchangeable. One gives you a compound assignment that cannot be chained and silently discards work. One gives you a binary operator returning a reference to storage that has already gone, which this compiler warns about and then runs correctly anyway, and which AddressSanitizer will not report unless you turn on an option that is off by default. Only the third is right, and it costs nothing — measured byte-identical to addition written by hand.

Intermediate18 min readC++Part IFundamentalsOperators

Part I · Module 15 · Chapter 15.3 · Arithmetic and Compound Assignment Operators

1. Two Operators, One Idea

For every arithmetic operation a type supports, a reader expects two spellings:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    c = a + b;        // produce a new value, leave a and b alone
    a += b;           // modify a in place

Chapter 4.6 §5 established what compound assignment means for built-in types, and the relationship a reader relies on:

a += b must leave a holding exactly what a = a + b would have left it holding.

For int that is guaranteed by the language. For your type it is guaranteed by nothing at all. Two functions, checked separately, with no rule connecting them — so the agreement is a property you have to create rather than one you can assume.

2. The Drift, Measured

Two types. The first derives + from +=. The second writes them independently, and its += applies a saturation rule its + does not:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct Bad {
        long c = 0;
        Bad& operator+=(const Bad& r) { c += r.c; if (c > 1000) c = 1000; return *this; }  // saturates
    };
    Bad operator+(const Bad& a, const Bad& b) { return Bad{a.c + b.c}; }                   // does not

Checking a += b against a = a + b for every pair over a wide range of values:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Good  (+ derived from +=) :     0 disagreement(s) out of 19425 pairs
    Bad   (+ written separately): 12273 disagreement(s) out of 19425 pairs

12,273 of 19,425. Both operators compile. Neither produces a warning at any level. Every individual function is internally correct — Bad's += saturates exactly as written, and its + adds exactly as written. The defect exists only in the relationship, which is why nothing local detects it.

And the fix is not "be careful". It is structural, because the derived version's 0 disagreements are not a test result — they are a consequence. Good's + calls +=, so there is one implementation of the rule and no second place for it to differ.

3. Which One Carries the Logic

The derivation direction is the part people reverse. The instinct is to write + first — it feels more fundamental, like the mathematical operation — and then define += as a = a + b. That is backwards, for two reasons.

+= is the one that can be written without the other. It modifies an object in place using the object's own members. + needs to produce a new object, which means constructing one, which is extra work that has nothing to do with the arithmetic.

And defining += in terms of + costs a copy. *this = *this + rhs constructs a temporary and then assigns it over the object. Writing it the other way round, as §5 shows, costs nothing measurable.

So:

Write += as the only implementation. Derive + from it.

The same applies to every pair: -=/-, *=/*, /=//, %=/%, &=/&, |=/|, ^=/^, <<=/<<, >>=/>>.

4. The Canonical Pair

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct Money {
        long cents = 0;
        // the COMPOUND form carries the logic -- a MEMBER, since it modifies the object
        Money& operator+=(const Money& rhs) { cents += rhs.cents; return *this; }
        Money& operator-=(const Money& rhs) { cents -= rhs.cents; return *this; }
    };
    // the BINARY form is derived -- a NON-MEMBER, taking the left operand BY VALUE
    Money operator+(Money lhs, const Money& rhs) { lhs += rhs; return lhs; }
    Money operator-(Money lhs, const Money& rhs) { lhs -= rhs; return lhs; }

Four decisions in those six lines, three of them already established:

+= is a member — Chapter 15.2 §8: it modifies its left operand, which is always the object, so symmetry buys nothing.

+ is a non-member — Chapter 15.2 §4: measured, a member version rejects a literal on the left.

+= returns Money& and + returns Money — §6 and §7 measure what the alternatives do.

And + takes its left operand by value, which is the one idiom that looks like a mistake and is not. The parameter lhs is already a copy, so lhs += rhs modifies that copy and returns it — no second temporary, and the copy you needed in order to produce a new value is the parameter itself. §9 measures its cost.

5. The Trace

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A. a + b leaves the operands alone:
     a=250  b=175  c=425
B. a += b modifies a and returns it:
     a=425  d=425   (a += b returned a reference to a)
     &d == &a ? 0   (d is a COPY, built from the returned reference)
C. chaining, which needs += to return a reference:
     e = 775

A is the essential difference: + produced a third value and left both operands untouched.

B shows += doing the opposite — a changed. And the &d == &a ? 0 line is worth pausing on: operator+= returned a reference to a, but Money d = (a += b) copy-initialises d from that reference, so d is a separate object. The reference is what makes the expression usable, not what d becomes.

C is why the reference return matters. ((e += a) += b) += b starting from 0 with a=425 and b=175 gives 775, because each += returned something further += could be applied to — and applied to the same object each time.

6. Return Type One: By Value from +=

The first of the three wrong-looking-right choices. Return Money from += instead of Money&:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct ByValue {
        long c = 0;
        ByValue operator+=(const ByValue& r) { c += r.c; return *this; }   // NOT a reference
    };

Chained three times, adding one each time:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A. += returning BY VALUE, chained:
     a.c = 1   (three += applied; expected 3 if chaining accumulated)
B. += returning a REFERENCE, chained:
     b.c = 3

One, not three. The first += modified a and returned a copy. The second += modified that copy. The third modified a copy of the copy. Two of the three operations were applied to temporaries and discarded.

Zero warnings, at any level. Chapter 4.6 §3 established that assignment produces a value; what it produces for built-ins is a reference to the assigned-to object, and a by-value return quietly breaks that.

This is the chapter's only silent wrong answer — the other two failures are a diagnostic and a memory bug.

7. Return Type Two: A Reference from +

The symmetric error, and the one with teeth. Return const Money& from +:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    const Money& operator+(Money lhs, const Money& rhs) { lhs += rhs; return lhs; }

lhs is a by-value parameter — it is destroyed when the function returns, so the reference refers to storage that is gone. Three results, in the order you would encounter them.

The compiler warns, and the warning is excellent:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: reference to stack memory associated with parameter 'lhs' returned [-Wreturn-stack-address]

The program then prints the right answer:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
read through the returned reference: 425  (expected 425)
exit: 0

And AddressSanitizer says nothing — until you enable an option that is off by default:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ASan, default                                    : silent, exit 0
ASAN_OPTIONS=detect_stack_use_after_return=1     :
    ERROR: AddressSanitizer: stack-use-after-return
    SUMMARY: ... badreturn.cpp:13 in main
    exit 134

So the diagnostic chain is exactly inverted from what you would hope: the compile-time warning is the reliable detector, the run is misleading, and the sanitizer needs configuring. Chapter 14.4 §10's callout made this argument about alloc_dealloc_mismatch and Chapter 14.6 §11 generalised it — when a tool is silent, confirm it could have spoken. This is the third instance in two modules.

The practical rule is simpler than the analysis: a binary arithmetic operator returns by value, always, because the value it produces is new and nothing else owns it. Chapter 8.8 §3 measured the general form of this dangle.

8. Return Type Three: Correct

OperatorReturnsWhy
a += bT&the object already exists and the result is the object
a + bTthe result is a new value nothing else owns
++aT&the object is modified and the result is the object
a++Tthe result is the old value, which no longer exists in the object
unary -aTa new value

The pattern: return a reference when the result already exists as an object; return a value when you just made it.

And const on the return of + is a historical habit worth dropping — const T prevents moving from the result, which Chapter 13.8's move semantics make undesirable. Return a plain T.

9. The By-Value Left Operand Costs Nothing

§4's Money operator+(Money lhs, const Money& rhs) takes its left operand by value, which looks like a gratuitous copy. At -O2:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    long viaCanonical(Money a, Money b) { return (a + b).cents; }
    long byHand      (Money a, Money b) { return a.cents + b.cents; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
__Zpl5MoneyRKS_           (3): ldr x8,[x1] | add x0,x8,x0 | ret
__Z12viaCanonical5MoneyS_ (2): add x0,x1,x0 | ret
__Z6byHand5MoneyS_        (2): add x0,x1,x0 | ret

Byte-identical to addition written by hand — two instructions. The copy the idiom appears to make is the copy the operation genuinely needs, and the optimiser removes the rest. Chapter 15.1 §4 measured the same thing for the operator mechanism itself.

(There is a refinement for types where copying is expensive: a second overload taking the left operand as T&& can reuse a temporary's storage. That depends on Chapter 13.8's move semantics and is a deliberate omission here — the by-value form is correct and free for the ordinary case, which is the one this chapter is about.)

10. Increment and Decrement

Two operators with two forms each, and the forms must be distinguished somehow. The language's answer is a dummy parameter:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Money& operator++()    { ++cents; return *this; }           // PREFIX
    Money  operator++(int) { Money old = *this; ++cents; return old; }   // POSTFIX

The int is a tag, not a value. It is never passed and never named; its only job is to give the postfix form a different signature. This is the one place in C++ where a parameter exists purely to disambiguate.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
D. prefix vs postfix ++ :
     after pre  = ++p : p=11  pre=11
     after post = q++ : q=11  post=10

Both modified the object to 11. The prefix form yielded 11 — the new value, by reference. The postfix form yielded 10 — the old value, by value, which is why it must copy. Chapter 4.7 §2 established exactly this for built-ins.

And that copy is the reason for Chapter 4.7 §5's advice: prefer prefix unless you need the old value, because postfix constructs an extra object. For an int the compiler erases the difference; for a type with real state it may not.

11. What to Actually Do

Write += and derive + from it — one implementation, so they cannot disagree.

Never write += as *this = *this + rhs — the derivation runs the other way and costs a copy.

+= is a member returning T&; + is a non-member returning T.

Take +'s left operand by value — measured byte-identical to hand-written addition.

Do not return const T from + — it blocks moving from the result.

Give postfix ++ an unnamed int tag, return the old value by value, and prefer prefix.

And write the agreement check: for a range of values, assert a += b leaves a equal to a = a + b. It is the only automatic detector of the one failure nothing else catches.

12. Mistakes Worth Making Once

Writing + and += independently. Measured: 12,273 disagreements out of 19,425 with zero diagnostics.

Deriving in the wrong direction. *this = *this + rhs costs a temporary and an assignment.

Returning T from +=. Measured: chained three times, gave 1 instead of 3, silently.

Returning T& or const T& from +. A dangling reference; -Wreturn-stack-address catches it, the run prints 425, and ASan is silent by default.

Trusting the clean run over the warning. Here the compile-time diagnostic is the reliable one.

Returning const T from +. Prevents moving from the result.

Making + a member. Chapter 15.2 §2 — a literal on the left is rejected.

Making += a non-member. Legal, but it always modifies the object.

Naming the postfix int parameter and using it. It is a tag; nothing is passed.

Using postfix ++ out of habit in a loop that discards the result — Chapter 4.7 §5.

13. Interview Relevance

"How do you implement + and += for a type?" += as a member returning T&, carrying the logic; + as a non-member returning T, taking the left operand by value and calling +=. Naming the derivation direction is the discriminator.

"Why that direction?" += can be written without +, and defining += via + costs a temporary and an assignment.

"What does operator+ return, and why not a reference?" By value — the result is new. A reference would refer to the by-value parameter, which is destroyed at return.

"What happens if += returns by value?" Chaining silently stops accumulating — measured 1 instead of 3, with no warning. Few candidates have seen this.

"How are prefix and postfix ++ distinguished?" An unnamed int parameter on the postfix form. Prefix returns T&, postfix returns T holding the old value.

"Why prefer prefix?" Postfix must copy the old value; prefix does not.

"How would you check that + and += agree?" Compare a += b against a = a + b over many values — the answer that shows you know nothing else will catch a disagreement.

14. What You Should Be Able to Say Now

  1. Every arithmetic idea needs two operators, and a reader assumes a += b equals a = a + b.
  2. Nothing in the language enforces that for your type.
  3. Measured: independently written operators disagreed 12,273 times out of 19,425 value pairs — with zero diagnostics.
  4. Measured: the derived version disagreed 0 times, and that is a consequence of having one implementation, not a test result.
  5. += carries the logic; + is derived — because += needs nothing else, and the reverse derivation costs a temporary.
  6. += is a member returning T&; + is a non-member returning T.
  7. + takes its left operand by value, and the copy it appears to add is the one the operation needs.
  8. Measured: that idiom is byte-identical to hand-written addition — 2 instructions.
  9. Measured: += returning by value made a chained expression yield 1 instead of 3, silently.
  10. Measured: + returning a reference gave -Wreturn-stack-address, then printed 425 correctly, and ASan was silent by default.
  11. Only detect_stack_use_after_return=1 reported it — stack-use-after-return, exit 134.
  12. Return a reference when the result already exists; a value when you just made it.
  13. Do not return const T from +.
  14. Postfix ++ is distinguished by an unnamed int tag, returns the old value by value, and therefore copies.
  15. Measured: ++p gave p=11 pre=11; q++ gave q=11 post=10.
  16. The agreement check is the only automatic detector of the one failure no tool catches.

15. Exercises

Exercise 1 — the canonical pair. Write += and + for a Duration holding nanoseconds, with the correct placements and return types.

Exercise 2 — the direction. Explain why += is the implementation and + the derivation, giving both reasons.

Exercise 3 — predict the drift. A Level whose += clamps to 100 and whose + does not. Give the result of a += b and a = a + b for a=80, b=50, and say what detects the discrepancy.

Exercise 4 — write the check. Write the agreement check for Duration over a range of values, and say what it would catch that the compiler cannot.

Exercise 5 — the three return types. For += returning T, + returning const T&, and the correct pair, say what each does: compile result, run result, and diagnostic.

Exercise 6 — chaining. Explain why ((e += a) += b) += b gave 775 with a reference return and what it would give with a by-value return.

Exercise 7 — the tag. Write prefix and postfix -- for Duration and say what the int is for and why postfix must copy.

Exercise 8 — cost. A colleague objects that Money operator+(Money lhs, const Money& rhs) makes an unnecessary copy. Give the measured answer.

Exercise 9 — which tool. For each of the chapter's three failures, name the stage it is caught at and the tool, or say that nothing catches it.

16. Summary

  • Two operators per idea, and a reader assumes they agree. Nothing enforces it.
  • Measured: 12,273 disagreements out of 19,425 pairs from independently written operators, with zero diagnostics; 0 from the derived version.
  • Write += as the only implementation and derive + — the reverse direction costs a temporary.
  • += is a member returning T&; + is a non-member returning T with the left operand by value.
  • Measured: that idiom compiles to 2 instructions, byte-identical to hand-written addition.
  • Measured: by-value += made a chained expression give 1 instead of 3, silently.
  • Measured: a reference-returning + gave -Wreturn-stack-address, then printed the right answer, with ASan silent until detect_stack_use_after_return=1.
  • Reference out when the result exists; value out when you made it. Postfix ++ takes an unnamed int tag and copies.
  • The agreement check is the only automatic detector of the drift.

Arithmetic is the easy half of the consistency problem, because + and += are two operators and there are only two ways for two things to disagree. Comparison is six operators — ==, !=, <, <=, >, >= — which a reader expects to form a single coherent story: that exactly one of less-than, equal, greater-than holds for any pair, that != is the negation of ==, and that <= is < or ==. Six hand-written functions give that story many ways to go wrong, and the next chapter measures one of them doing something none of this chapter's failures did: a comparison operator that compiles, reads correctly, passes casual testing, and drives a standard-library algorithm into reading past the end of its array. Then the C++20 feature that replaces all six with one line, along with the precise measurement of what that line does and does not generate — because the obvious assumption about it is wrong.

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.