C++ Part I · Module 4 · Expressions and Operators
Increment and Decrement
The smallest operators in the language add or subtract one, and they look like pure shorthand for the compound assignment in the previous chapter. They are not quite, because each comes in two forms. Both forms have exactly the same effect on the object, and they produce different values — one gives you the value after the change and the other gives you the value before it. When you ignore the result, which is most of the time, the two are interchangeable and the choice is cosmetic. When you use the result, the difference is the whole point. And on types more complicated than a plain number, one of the two forms has to do extra work to deliver what it promises, which is the reason experienced C++ programmers have a default. This chapter also notes something that genuinely changed between C++14 and C++17, because a line you may have been told is undefined is not any more — and is still not worth writing.
Foundation12 min readC++Part IFundamentalsOperators
Part I · Module 4 · Chapter 4.7 · Increment and Decrement
1. Two Operators, Two Forms Each
++a // prefix increment
a++ // postfix increment
--a // prefix decrement
a-- // postfix decrementAll four change the object by one. ++a and a++ both leave a one larger.
The difference is in what the expression produces, which Chapter 4.1 §2 established is a separate question from what it does.
2. Which Value You Get
#include <iostream>
int main()
{
int a = 5;
std::cout << "a = 5; ++a yields " << ++a << ", a is now " << a << '\n';
int b = 5;
std::cout << "b = 5; b++ yields " << b++ << ", b is now " << b << '\n';
return 0;
}Expected output:
a = 5; ++a yields 6, a is now 6
b = 5; b++ yields 5, b is now 6Both objects ended at 6. The expressions produced different values:
Prefix (
++a) produces the value after the change. Postfix (a++) produces the value before it.
A way to remember it that does not require memorising: read the expression left to right. In ++a the increment comes first, so you get the incremented value. In a++ the value comes first, so you get the old one.
Decrement works identically: --a gives the decreased value, a-- gives the original.
3. When the Difference Does Not Matter
As a statement on its own, the result is discarded — Chapter 4.1 §5 — so the two forms are interchangeable:
++error_count; // same effect
error_count++; // same effectThis is the common case. In a loop that counts, nothing uses the value, and either form is correct.
4. When It Does
The moment you use the result, they are different operators:
int index = 0;
int first = buffer[index++]; // uses 0, then index becomes 1
int second = buffer[++index]; // index becomes 2, then uses 2That index++ idiom — use the current value and advance — is common and compact. It is also the form most likely to confuse a reader six months later, because the effect and the value happen in an order the eye does not naturally supply.
The alternative is almost always clearer:
int first = buffer[index];
++index;Two lines, no ambiguity, identical behaviour. When the result matters, consider not using the result.
5. Why Prefix Is the Default
For an int, the two forms cost the same and no reasonable compiler produces different code for ++i; and i++; as statements. The habit of preferring prefix comes from what happens on types that are not plain numbers.
Think about what postfix has to promise. It must produce the value the object had before the change, and it must change the object. Those two things cannot both be true of one object, so postfix has to keep the old value somewhere — it makes a copy, changes the original, and gives you the copy.
Prefix has no such obligation. It changes the object and produces the object's new value. Nothing needs to be preserved.
For an int a "copy" is a register move and the difference vanishes. For a type where copying means real work — and from Module 13 onward you will meet types where it means allocating memory — that copy is not free, and it is being made to produce a value you are about to discard.
So the convention is:
Write
++irather thani++when the result is not used. It is never worse, and on some types it is better.
This matters most with iterators, which Module 20 introduces and which are incremented constantly. Module 13 is where copying becomes a subject with a cost. Until then, treat prefix as the default and know that the reason is real rather than stylistic.
6. A Line That Changed Meaning in C++17
A note on something you may have been taught, because the rule genuinely changed.
int i = 5;
i = i++; // what does this do?For many years the answer was "this is undefined behaviour" — it modified i twice in one expression with no sequencing between the modifications. That was correct for C++14 and earlier.
C++17 changed the sequencing rules for assignment. In E1 = E2, the right-hand side is now sequenced before the left, which gives this expression a defined meaning.
Measured on the machine used for this chapter:
$ c++ -std=c++14 -Wall -Wextra -Wpedantic seq.cpp -o seq
seq.cpp:5:10: warning: multiple unsequenced modifications to 'i' [-Wunsequenced]
$ c++ -std=c++17 -Wall -Wextra -Wpedantic seq.cpp -o seq
$ ← no diagnosticA warning under C++14 and silence under C++17, from the same source. The sequencing rule changed, and the compiler tracks it.
Two things follow, and the second matters more.
Not every such expression became defined. This one still has not:
int r = i++ + i++; // still undefined behaviourseq2.cpp:5:14: warning: multiple unsequenced modifications to 'i' [-Wunsequenced]C++17 pinned down the order for some operators and left others alone. Chapter 4.9 covers which, and is where sequencing belongs as a subject.
Being defined does not make it worth writing. i = i++ now has a specified result, and no reader can work out what that result is without looking up a sequencing rule that changed between standard versions. Code whose meaning depends on which -std flag was used is code that will be misread.
The rule to take away is not "
i = i++is fine now". It is: change an object once per expression, and if you cannot tell at a glance how many times you changed it, split the expression up.
7. Mistakes Worth Making Once
Mixing up which form gives which value. Prefix gives the new value, postfix the old. The symbol's position tells you.
Using the result when you did not need to. buffer[index++] is compact and reliably misread. Two lines cost nothing.
Writing i++ as a statement out of habit. Identical for an int, and the wrong habit for the types in Modules 13 and 20.
Modifying an object twice in one expression. Some such expressions are defined in C++17 and others are not, and a reader cannot tell them apart. Do not write either kind.
Reading "it is defined in C++17" as "it is fine". Defined means the standard specifies a result. It says nothing about whether a human will predict that result correctly.
Assuming an older rule still applies. If you learned that i = i++ is undefined, that was true and is now version-dependent. This is the situation Chapter 1.3 was written for: know which standard your claim belongs to.
8. What You Should Be Able to Say Now
++and--each have a prefix and a postfix form; all four change the object by one.- Prefix produces the value after the change; postfix produces the value before it. Measured:
++ayielded 6 andb++yielded 5, with both objects ending at 6. - As a standalone statement the result is discarded and the forms are interchangeable.
- When the result is used they are different operators, and the idiom
buffer[index++]is compact and easy to misread. - Postfix must produce the old value, so it has to keep a copy; prefix has no such obligation.
- For an
intthe difference is not measurable. For types where copying is real work it is, which is why prefix is the default. - That matters most for iterators (Module 20) and types with real copy cost (Module 13).
- C++17 changed assignment sequencing, so
i = i++is defined in C++17 and was undefined before. Measured: a-Wunsequencedwarning under-std=c++14and none under-std=c++17. i++ + i++is still undefined — measured, it still warns under C++17. Chapter 4.9 owns sequencing.- Defined is not the same as readable. Change an object once per expression.
9. Exercises
Exercise 1 — predict four results. With int n = 10; each time, say what the expression produces and what n holds afterwards: (a) ++n (b) n++ (c) --n (d) n--.
Exercise 2 — trace the index. With index = 0 and buffer holding {10, 20, 30}, say which element each line reads and what index is afterwards: (a) buffer[index++] (b) buffer[++index].
Exercise 3 — rewrite for a reader. Rewrite total += buffer[index++]; as two statements with identical behaviour, and say which version you would put in a code review and why.
Exercise 4 — explain the cost. Explain, without using the word "performance", why postfix increment has to do something prefix does not. Then say why that costs nothing for an int.
Exercise 5 — which standard? A colleague says i = i++ is undefined behaviour. Under what circumstances are they right, under what circumstances are they wrong, and what would you check to find out which applies to your build?
Exercise 6 — defined but not fine. i = i++ has a defined result in C++17. Give two reasons it should still not appear in a codebase.
Exercise 7 — spot the real problem. Which of these modifies an object more than once in a single expression, and which of those is still undefined in C++17? (a) i = i + 1 (b) i = i++ (c) i++ + i++ (d) i++, i++
10. Summary
- Four operators, all changing the object by one; the forms differ in the value produced.
- Prefix gives the new value, postfix the old. Measured:
++a→ 6,b++→ 5, both ending at 6. - As a statement they are interchangeable, because the result is discarded.
- Using the result makes them different operators — and
buffer[index++]is compact and easy to misread. - Postfix must preserve the old value, so it copies. Prefix does not.
- For an
intthat is free; for other types it is not — so prefix is the default, and the habit matters by Modules 13 and 20. - C++17 changed assignment sequencing.
i = i++warned under-std=c++14and is silent under-std=c++17. i++ + i++is still undefined and still warns.- Defined is not readable. Change an object once per expression.
The operators remaining are the ones that work on bit patterns rather than on numbers, and they are the tool Chapter 3.4 §5 said was needed and could not teach. A register is not a quantity — it is a row of bits with named fields packed into it, and getting one of those fields out, changing it and putting it back is the most common thing an engineer does with a hardware value. Chapter 4.8 is those operators, the shift counts that make a result undefined, and a trap that catches people the first time they invert a mask.
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.
