Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 18 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free: 18 minutes

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.

Four expressions and their order: << left to right, = right side first, call arguments in either order, i++ + ++i in no order.Which part is evaluated firststd::cout << a << bleft to right since C++17abx = valuethe right side first since C++17valuexf(p(), q())f first; p() and q() in either orderfp()q()either order,never mixedi++ + ++ino order at alli++++iboth change i:undefined behaviour

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.

  1. std::cout << a << b: a, then b. Since C++17 the operands of << and >> are evaluated from left to right.
  2. x = value: value, then x. Since C++17 the right side of an assignment is evaluated before the left.
  3. 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.
  4. 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

exam.cpp C++ · exam.cpp
#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); }
}
Five undefined expressions, two compilers Python · exam_session.py
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 << b and a >> b, a is evaluated before b, so std::cout << f() << g() calls f first.
  • In an assignment a = b, and in the compound assignments such as a += b, the right side b is evaluated, side effects included, before the left side a.
  • In a[b], a is evaluated before b.
  • In a call f(args), the function itself, for example obj.method in obj.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.
cpp17_order.cpp C++ · cpp17_order.cpp
#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);
}
Expressions that C++17 defines Python · cpp17_session.py
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

arguments.cpp C++ · arguments.cpp
#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)
}
Two calls in one argument list Python · arguments_session.py
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:

rewritten.cpp C++ · rewritten.cpp
#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); }
}
One change per statement Python · rewritten_session.py
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.

  1. Question 1 of 10 Under C++17 and later, how is this expression classified?

    Read the code, then choose one answer.

    i = i++ + ++i;
    Show the answer to question 1

    Answer: Undefined behaviour

    The operands of + are unsequenced, and both change i. The assignment's own rule does not help, because the problem is inside the right side.

  2. Question 2 of 10 Under C++17 and later, how is this expression classified?

    Read the code, then choose one answer.

    std::cout << i++ << ' ' << i++;
    Show the answer to question 2

    Answer: Well-defined

    Since C++17 the left operand of << is evaluated before the right one, so the first i++ completes before the second: with i = 0 it prints 0 1.

  3. Question 3 of 10 Under C++17 and later, how is this expression classified?

    Read the code, then choose one answer.

    show(i++, i++);
    Show the answer to question 3

    Answer: Unspecified order, but not undefined

    Since C++17 each argument is evaluated completely before the next, so the two increments cannot overlap, but which argument goes first is unspecified: show(0, 1) and show(1, 0) are both allowed.

  4. Question 4 of 10 Under C++17 and later, how is this expression classified?

    Read the code, then choose one answer.

    v[i] = i++;
    Show the answer to question 4

    Answer: Well-defined

    Since C++17 the right side of = is evaluated first, including the increment, so v[i] uses the new value of i.

  5. Question 5 of 10 Under C++17 and later, how is this expression classified?

    Read the code, then choose one answer.

    int s = a[i] + a[i++];
    Show the answer to question 5

    Answer: Undefined behaviour

    The left operand reads i and the right one modifies it, and the operands of + are unsequenced, so the read and the change are unsequenced.

  6. Question 6 of 10 Under C++17 and later, how is this expression classified?

    Read the code, then choose one answer.

    x = ++i + 1;
    Show the answer to question 6

    Answer: Well-defined

    i is changed once and not read anywhere else in the expression, so there is nothing to conflict with.

  7. Question 7 of 10 Under C++17 and later, how is this expression classified?

    Read the code, then choose one answer.

    total(price(), tax());
    Show the answer to question 7

    Answer: Unspecified order, but not undefined

    Function arguments are indeterminately sequenced. If price() and tax() print something or share state, the order of their effects is unspecified, but the program is not undefined.

  8. Question 8 of 10 Under C++17 and later, how is this expression classified?

    Read the code, then choose one answer.

    int r = i++ * i++;
    Show the answer to question 8

    Answer: Undefined behaviour

    Two unsequenced changes to i in the operands of *. Both compilers of this lesson warn about it.

  9. Question 9 of 10 Under C++17 and later, how is this expression classified?

    Read the code, then choose one answer.

    n = n++ + 1;
    Show the answer to question 9

    Answer: Well-defined

    Since C++17 the right side, with the side effect of n++, is complete before the assignment stores into n. With n = 3, n becomes 4.

  10. Question 10 of 10 Under C++17 and later, how is this expression classified?

    Read the code, then choose one answer.

    (log_start(), work());
    Show the answer to question 10

    Answer: Well-defined

    The left operand of the built-in comma operator is sequenced before the right one, so log_start() always runs first. Commas that separate function arguments are a different thing.

Key takeaways

  • Changing a variable twice, or changing it and reading it, in one expression without sequencing is undefined behaviour: i++ + ++i has no correct output.
  • The two compilers here disagree on ++i + ++i (13 against 14), i + i++ (10 against 11) and m * m++ (4 against 6), and both are allowed to.
  • C++17 evaluates a << b and a[b] from left to right, a = b right 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 -Wunsequenced by default; GCC warns with -Wsequence-point under -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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.