C++ Module 2 – Types, values and expressions
Why i++ + ++i has no single answer in C++
Why i++ + ++i is undefined behaviour, what C++17 changed about evaluation order, and how GCC and Clang warn about unsequenced expressions.
What you will learn
- Identify unsequenced modifications that make behaviour undefined
- Explain what C++17 changed about evaluation order
- Use compiler warnings to find order-of-evaluation bugs
Before you start
What you’ll learn · Before you start
What you will learn
- Identify unsequenced modifications that make behaviour undefined
- Explain what C++17 changed about evaluation order
- Use compiler warnings to find order-of-evaluation bugs
Before you start
On this page
i++ + ++i changes i twice in one expression, and nothing in the language says which change happens first: the
two operands of + are unsequenced. The C++ standard calls that undefined behaviour, so there is no correct
output to predict. Compilers are not even required to be consistent, and this lesson shows Apple clang 21 and GCC 16
printing different numbers for several classic expressions of this kind. C++17 did fix the order for some operators:
the left operand of << and >> is evaluated first, the right side of an assignment before the left, and the object
of a member call before its arguments. Function arguments are still evaluated in an unspecified order, which is not
undefined, but still means two compilers may legitimately disagree. When a test or an interviewer asks for the output
of such an expression, “undefined behaviour” is the correct answer, and the fix is to write one change per statement.
Sequenced, unsequenced and indeterminately sequenced
Evaluating an expression does two kinds of work: value computations, which work out values, and side effects, such as storing a new value into a variable. The standard describes their order with three relations:
- A is sequenced before B: A, with all its side effects, is complete before B starts. The end of each full
expression, normally the semicolon, is such a point, and so is the left operand of
&&,||,?:and the comma operator. - A and B are indeterminately sequenced: one of them runs entirely before the other, but which one is unspecified, and may even differ between two evaluations of the same expression.
- A and B are unsequenced: no order at all, and their execution may overlap. Unless a rule says otherwise, the operands of an operator are unsequenced.
The rule that matters follows from the last one. If a side effect on a variable is unsequenced relative to another side effect on the same variable, or to a use of its value, the behaviour is undefined.
Which part of an expression is evaluated first
Text description of the diagram
Four rows, each an expression with boxes for its parts and arrows from what is evaluated first to what comes next.
- std::cout << a << b: a, then b. Since C++17 the operands of << and >> are evaluated from left to right.
- x = value: value, then x. Since C++17 the right side of an assignment is evaluated before the left.
- f(p(), q()): f first, then p() and q() in either order. The order of the arguments is unspecified, but one call finishes before the other starts.
- i++ + ++i: the two operands are joined by a dashed line with no arrow. They are unsequenced, and because both change i, the behaviour is undefined.
The classic expressions on two compilers
#include <print>
int main() {
{ int i = 1; int r = i++ + ++i; std::println("i++ + ++i (i = 1): {}", r); }
{ int i = 5; int r = ++i + ++i; std::println("++i + ++i (i = 5): {}", r); }
{ int i = 5; int r = i + i++; std::println("i + i++ (i = 5): {}", r); }
{ int m = 2; int r = m * m++; std::println("m * m++ (m = 2): {}", r); }
{ int j = 1; j = j++ + ++j; std::println("j = j++ + ++j (j = 1): {}", j); }
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra exam.cpp -o exam
./exam
g++-16 -std=c++23 -Wall -Wextra exam.cpp -o exam
./exam
""", max_lines=60) Output
$ clang++ -std=c++23 -Wall -Wextra exam.cpp -o exam
exam.cpp:4:27: warning: multiple unsequenced modifications to 'i' [-Wunsequenced]
4 | { int i = 1; int r = i++ + ++i; std::println("i++ + ++i (i = 1): {}", r); }
| ^ ~~
exam.cpp:5:26: warning: multiple unsequenced modifications to 'i' [-Wunsequenced]
5 | { int i = 5; int r = ++i + ++i; std::println("++i + ++i (i = 5): {}", r); }
| ^ ~~
exam.cpp:6:31: warning: unsequenced modification and access to 'i' [-Wunsequenced]
6 | { int i = 5; int r = i + i++; std::println("i + i++ (i = 5): {}", r); }
| ~ ^
exam.cpp:7:31: warning: unsequenced modification and access to 'm' [-Wunsequenced]
7 | { int m = 2; int r = m * m++; std::println("m * m++ (m = 2): {}", r); }
| ~ ^
exam.cpp:8:23: warning: multiple unsequenced modifications to 'j' [-Wunsequenced]
8 | { int j = 1; j = j++ + ++j; std::println("j = j++ + ++j (j = 1): {}", j); }
| ^ ~~
5 warnings generated.
$ ./exam
i++ + ++i (i = 1): 4
++i + ++i (i = 5): 13
i + i++ (i = 5): 10
m * m++ (m = 2): 4
j = j++ + ++j (j = 1): 4
$ g++-16 -std=c++23 -Wall -Wextra exam.cpp -o exam
exam.cpp: In function 'int main()':
exam.cpp:4:27: warning: operation on 'i' may be undefined [-Wsequence-point]
4 | { int i = 1; int r = i++ + ++i; std::println("i++ + ++i (i = 1): {}", r); }
| ~^~
exam.cpp:5:26: warning: operation on 'i' may be undefined [-Wsequence-point]
5 | { int i = 5; int r = ++i + ++i; std::println("++i + ++i (i = 5): {}", r); }
| ^~~
exam.cpp:6:31: warning: operation on 'i' may be undefined [-Wsequence-point]
6 | { int i = 5; int r = i + i++; std::println("i + i++ (i = 5): {}", r); }
| ~^~
exam.cpp:7:31: warning: operation on 'm' may be undefined [-Wsequence-point]
7 | { int m = 2; int r = m * m++; std::println("m * m++ (m = 2): {}", r); }
| ~^~
exam.cpp:8:20: warning: operation on 'j' may be undefined [-Wsequence-point]
8 | { int j = 1; j = j++ + ++j; std::println("j = j++ + ++j (j = 1): {}", j); }
| ~~^~~~~~~~~~~
exam.cpp:8:20: warning: operation on 'j' may be undefined [-Wsequence-point]
exam.cpp:8:20: warning: operation on 'j' may be undefined [-Wsequence-point]
exam.cpp:8:20: warning: operation on 'j' may be undefined [-Wsequence-point]
$ ./exam
i++ + ++i (i = 1): 4
++i + ++i (i = 5): 14
i + i++ (i = 5): 11
m * m++ (m = 2): 6
j = j++ + ++j (j = 1): 4
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exam_session.py
Read the session as a terminal: a $ marks each command, and its output follows. Every line of exam.cpp modifies a variable twice, or modifies it and reads it, without sequencing. Both compilers
say so: Apple clang with -Wunsequenced, which is on by default, and GCC with -Wsequence-point, which -Wall
enables. The outputs then show what undefined behaviour means in practice. Two compilers on the same machine agree on
two lines and disagree on the other three: ++i + ++i gives 13 with Apple clang and 14 with GCC, i + i++ gives 10
and 11, and m * m++ gives 4 and 6. Neither compiler is wrong, because there is no right answer. Another compiler,
another version or another optimisation level may print something else again, and as with every undefined behaviour,
nothing obliges a compiler to produce a number at all.
What C++17 changed
Proposal P0145R3, adopted in C++17, gave an order to several expressions that idiomatic code writes all the time. Since C++17:
- In
a << banda >> b,ais evaluated beforeb, sostd::cout << f() << g()callsffirst. - In an assignment
a = b, and in the compound assignments such asa += b, the right sidebis evaluated, side effects included, before the left sidea. - In
a[b],ais evaluated beforeb. - In a call
f(args), the function itself, for exampleobj.methodinobj.method(args), is evaluated before any argument, and each argument is evaluated completely before the next one starts, in an unspecified order. - Overloaded operators follow the order of the built-in operator they replace, so
std::cout <<with your own types behaves the same way.
#include <iostream>
#include <map>
#include <print>
#include <string>
int main() {
int i = 0;
std::cout << i++ << ' ' << i++ << '\n'; // << runs left to right since C++17: prints 0 1
int v[3] = {7, 7, 7};
int k = 0;
v[k] = k++; // the right side first: v[1] = 0
std::println("v = {} {} {}", v[0], v[1], v[2]);
std::map<std::string, std::size_t> stock;
stock["apple"] = stock.size(); // size() runs before the new element is added
std::println("stock[\"apple\"] = {}", stock["apple"]);
int n = 3;
n = n++ + 1; // the right side, side effects included, before the store
std::println("n = {}", n);
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra cpp17_order.cpp -o cpp17_order
./cpp17_order
g++-16 -std=c++23 -Wall -Wextra cpp17_order.cpp -o cpp17_order
./cpp17_order
""") Output
$ clang++ -std=c++23 -Wall -Wextra cpp17_order.cpp -o cpp17_order
$ ./cpp17_order
0 1
v = 7 0 7
stock["apple"] = 0
n = 4
$ g++-16 -std=c++23 -Wall -Wextra cpp17_order.cpp -o cpp17_order
cpp17_order.cpp: In function 'int main()':
cpp17_order.cpp:8:33: warning: operation on 'i' may be undefined [-Wsequence-point]
8 | std::cout << i++ << ' ' << i++ << '\n'; // << runs left to right since C++17: prints 0 1
| ~^~
cpp17_order.cpp:12:13: warning: operation on 'k' may be undefined [-Wsequence-point]
12 | v[k] = k++; // the right side first: v[1] = 0
| ~^~
cpp17_order.cpp:20:7: warning: operation on 'n' may be undefined [-Wsequence-point]
20 | n = n++ + 1; // the right side, side effects included, before the store
| ~~^~~~~~~~~
cpp17_order.cpp:20:7: warning: operation on 'n' may be undefined [-Wsequence-point]
$ ./cpp17_order
0 1
v = 7 0 7
stock["apple"] = 0
n = 4
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cpp17_session.py
Both compilers print the same four lines, which the C++17 rules predict. std::cout << i++ << ' ' << i++ prints
0 1. v[k] = k++ evaluates the right side first, so k is already 1 when v[k] is chosen, and v[1] receives 0.
stock["apple"] = stock.size() calls size() before operator[] inserts the new element, so it stores 0. And
n = n++ + 1 is well-defined since C++17, because the increment belongs to the right side, which finishes before the
store: n ends up as 4.
The warnings differ, and the difference is documented. Apple clang follows the C++17 rules and is silent. GCC warns
about three of the four lines, because its manual says that -Wsequence-point keeps warning about code that C++17
defines, to help you avoid code that is still undefined in C and in older C++. Treat that as advice, and keep such
expressions out of code that anyone has to read twice.
Function arguments: unspecified, not undefined
#include <print>
int ticket = 0;
int next_ticket() { return ++ticket; }
void show(int first, int second) {
std::println("show({}, {})", first, second);
}
int main() {
show(next_ticket(), next_ticket()); // unspecified order: (1, 2) or (2, 1)
int a = next_ticket(); // separate statements fix the order
int b = next_ticket();
show(a, b); // always (3, 4)
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra arguments.cpp -o arguments
./arguments
g++-16 -std=c++23 -Wall -Wextra arguments.cpp -o arguments
./arguments
""") Output
$ clang++ -std=c++23 -Wall -Wextra arguments.cpp -o arguments $ ./arguments show(1, 2) show(3, 4) $ g++-16 -std=c++23 -Wall -Wextra arguments.cpp -o arguments $ ./arguments show(1, 2) show(3, 4)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 arguments_session.py
In show(next_ticket(), next_ticket()) the two calls are indeterminately sequenced: one finishes before the other
starts, so nothing is undefined, but the order is unspecified, and the call may print show(1, 2) or show(2, 1).
Both compilers on this Mac happened to evaluate from left to right in this build. That is an observation about one
build, not a promise: the standard lets each compiler choose, for each call, and rule ES.44 of the C++ Core
Guidelines says not to depend on it. The second half of the program shows the cure, which is boring and reliable:
compute the values in separate statements, in the order you want, and pass the results.
Rewriting the classics
Each classic expression stands for something its author probably meant. Written as separate steps, every one of them is well-defined, and both compilers agree:
#include <print>
int main() {
// i++ + ++i, written as the steps it was probably meant to be:
{ int i = 1; int old = i; ++i; ++i; int r = old + i; std::println("old i + new i: {}", r); }
// ++i + ++i:
{ int i = 5; ++i; int first = i; ++i; int r = first + i; std::println("6 + 7: {}", r); }
// i + i++:
{ int i = 5; int r = i + i; ++i; std::println("i + i, then ++i: {} (i is now {})", r, i); }
// m * m++:
{ int m = 2; int r = m * m; ++m; std::println("m * m, then ++m: {} (m is now {})", r, m); }
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra rewritten.cpp -o rewritten
./rewritten
g++-16 -std=c++23 -Wall -Wextra rewritten.cpp -o rewritten
./rewritten
""") Output
$ clang++ -std=c++23 -Wall -Wextra rewritten.cpp -o rewritten $ ./rewritten old i + new i: 4 6 + 7: 13 i + i, then ++i: 10 (i is now 6) m * m, then ++m: 4 (m is now 3) $ g++-16 -std=c++23 -Wall -Wextra rewritten.cpp -o rewritten $ ./rewritten old i + new i: 4 6 + 7: 13 i + i, then ++i: 10 (i is now 6) m * m, then ++m: 4 (m is now 3)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rewritten_session.py
The pattern is the one rule ES.43 of the C++ Core Guidelines recommends: change a variable at most once per
expression, and do not read it elsewhere in the same expression. ++i; on a line of its own is always fine; i++
inside a larger expression that also uses i almost never is.
Output questions in placement tests and exams
Placement tests and university exams sometimes ask for “the output” of expressions like i++ + ++i or
printf("%d %d", i++, i++). In C++, as in C, the honest answer to the first is that the behaviour is undefined;
for the second, that the order of the arguments is unspecified, and that modifying i twice there is undefined in
C. If the options include “undefined behaviour” or “compiler dependent”, that is the answer to choose, and the
session above, where two compilers printed different numbers, is the evidence.
Interview questions
Fresher · What is the difference between undefined, unspecified and implementation-defined behaviour?
All three describe what the standard leaves open. Implementation-defined behaviour must be chosen and documented by
each implementation: the size of long or whether plain char is signed. Unspecified behaviour is a choice among
allowed outcomes that need not be documented or consistent: the order in which function arguments are evaluated. In
both cases the program is still valid. Undefined behaviour puts no requirements on the program at all, as with
i++ + ++i, signed overflow or reading an uninitialised variable, so the compiler may assume it never happens.
Correct code may depend on implementation-defined choices if it checks them, should never depend on unspecified
ones, and must never contain undefined behaviour.
Check yourself
10 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Key takeaways
- Changing a variable twice, or changing it and reading it, in one expression without sequencing is undefined
behaviour:
i++ + ++ihas no correct output. - The two compilers here disagree on
++i + ++i(13 against 14),i + i++(10 against 11) andm * m++(4 against 6), and both are allowed to. - C++17 evaluates
a << banda[b]from left to right,a = bright side first, and a function before its arguments; overloaded operators follow the same rules. - Function arguments are indeterminately sequenced: not undefined, but in an unspecified order. Do not depend on it.
- Apple clang warns with
-Wunsequencedby default; GCC warns with-Wsequence-pointunder-Wall, and also about code that C++17 made well-defined. - One change per statement: rewrite the expression in steps, and the question “what does it print” has one answer.
References
- Working Draft, Programming Languages: C++ (N4950): [intro.execution], [expr.call], [expr.shift], [expr.ass], [over.match.oper] (ISO/IEC JTC1/SC22/WG21)
- P0145R3: Refining Expression Evaluation Order for Idiomatic C++ (ISO/IEC JTC1/SC22/WG21)
- Options to Request or Suppress Warnings (-Wsequence-point) (GNU Project)
- Diagnostic flags in Clang (-Wunsequenced) (LLVM Project)
- C++ Core Guidelines: ES.43, Avoid expressions with undefined order of evaluation (Standard C++ Foundation)
- C++ Core Guidelines: ES.44, Don't depend on order of evaluation of function arguments (Standard C++ Foundation)
Report a problem with this lesson
Kept only in this browser. Your Learn progress