Why this matters
Some one-line expressions look simple and have no clear answer. i = i++ looks like it should add one to i. a[k] = k++ looks like it should write to the slot before the increment. Interview questions use lines like these to test whether you know how C++ runs an expression. The answers depend on a set of rules that most programmers never read. This lesson explains those rules in plain terms. You will learn which parts of an expression run first, which orders the language fixes, which orders it leaves open, and why some expressions are not allowed to be written at all.
Operator precedence, which decides how the parts of an expression group together, is covered earlier in this module. This lesson is about something different: the order in which the parts run.
Precedence is not the same as order
Take this line:
int result = first() * second() + third();Precedence says that * groups before +. So the multiplication first() * second() is one unit, and third() is added to it. That is the grouping. Precedence says nothing about the order of the calls. The language does not say whether first() runs before second(), or whether third() runs before either of them.
Here is the difference in one sentence. Precedence decides which operator is applied to which operands. Evaluation order decides which operand is computed first. The two are separate rules.
Three words you need
The C++ standard uses three terms for the order of work inside an expression. They are easy to confuse, so here they are with plain meanings.
- Sequenced before means one step finishes completely before another step starts. If step A is sequenced before step B, then B can see every change A made.
- Unsequenced means the two steps have no order between them. The language does not promise either order.
- Undefined behaviour means the program breaks its own rules. The compiler may produce any result, including a crash, and it may not even be consistent from one run to the next.
Unsequenced steps are not always a bug. If two steps only read values, the order does not matter. The bug starts when one step changes a value and another step reads or changes the same value, with no order between them. That is the case that leads to undefined behaviour.
Example 1: operands with no fixed order
Start with a program that makes the order visible. Each function prints a line when it runs, so the output shows the order that this compiler chose.
The output on the machine that checked this lesson is:
left_value runs
right_value runs
sum = 3
first = 2, second = 1The first two lines show left_value running before right_value. That is what this compiler did, but the language does not promise it. The operands of + have no fixed order, so a different compiler, or the same compiler with different settings, could call right_value first.
The last line is more surprising. show_pair takes two arguments, and each argument calls next_id, which changes counter. The output says first is 2 and second is 1, so the second argument ran first. The first argument then got the later value. Again, this is one result from one compiler. The standard says the arguments of a function call are evaluated in an order that is not specified, and each argument is finished before the function body starts.
The rule to take from this example is simple. If two calls or two operands can change the same thing, do not write code that depends on which one runs first. Put each call on its own line.
Example 2: the orders that are fixed
Not every order is open. The standard fixes several orders, and you can rely on them. Three are common in everyday code.
The first is the comma operator. It evaluates the left part, then the right part, and gives the value of the right part. The next program uses it.
The output on the machine that checked this lesson is:
1 2
comma result = 30, e = 3
ok = 1, x = 5The first line prints 1 2. Since C++17 the << operator in a chain is evaluated from left to right, so the first call to next_id runs before the second one. Before C++17 the same line was not fixed. Under the newer rules you can read a chain of << as a list of steps that run in order.
The second line uses the comma operator inside parentheses. e += 2 runs first, so e becomes 3. Then e * 10 runs and gives 30. The result is 30, not 10, because the left part was finished before the right part started.
The third line uses &&. The left side (x = 5) > 3 runs first. Only if it is true does the right side x == 5 run. The lesson on guards with short-circuit covers this rule in full. Here it shows that x is 5 when the right side reads it, so the code is safe.
Other fixed orders include the condition of ?:, which runs before either branch, and the rule for = that the right side runs before the left side. The next section shows that rule, because it is the one that causes the most confusion.
Example 3: the right side of = runs first
Before you read this section, a warning. The next line is hard to read, and g++ warns about it. It is here so you can see what the rule does. You should not write code like this in a real program.
The output on the machine that checked this lesson is:
a[0] = 10, a[1] = 0, k = 1The output shows that the increment ran first. k++ returned 0 and set k to 1. Then a[k] used the new value of k, which is 1, so the assignment wrote 0 into a[1]. Slot a[0] kept its value of 10.
In C++17 and later, the standard says that in a = b, the right operand b is evaluated before the left operand a. This is why the increment ran before the index was computed. In C++14 and earlier, the same line had no fixed order, so the result was undefined.
You may wonder why g++ still warns about this line. The reason is that the line is easy to misread. Even when the standard defines the order, a careful program does not depend on it. Write the increment on its own line, before or after the use, so the next reader does not have to think about the rule.
Example 4: one change per expression is safe
Not every expression with ++ is a problem. The rule is about two things in the same expression touching the same variable. An expression that changes a variable once, and reads it only as part of that change, is safe. A common pattern is adding an item to an array and moving a counter at the same time.
The output on the machine that checked this lesson is:
size = 3, data = 10 20 30
old_count = 1, count = 2In data[size++] = 10, the index uses the old value of size, which is 0 the first time. The ++ then makes size equal to 1, so the next line writes to slot 1. There is only one change to size in each statement, and the value that is read is the same value that the increment returns. That is why the program is safe. The same reasoning applies to old_count = count++: the old value goes into old_count, and count goes up by one.
This pattern is common in real code, and it shows that the problem is not ++ itself. The problem starts when a second part of the same expression reads or changes the same variable with no order between them.
Checking an expression in three questions
When you are not sure whether an expression is safe, ask three questions in order.
- Does any part of the expression change a variable, with
++,--,=, or+=, or by calling a function that changes something? - If so, does any other part of the same expression read or change that same variable?
- If both are true, does the language fix the order between the two parts?
If the answer to the first question is no, or to the second question is no, the expression is safe. If the third answer is yes, as with &&, the comma operator, or the chain of << in C++17, the expression is safe under the fixed order. If the third answer is no, the expression is undefined, and you should split it into statements.
Apply the questions to three lines. data[size++] = 10 changes size once and reads it only inside that change, so the answer to question 2 is no. (x = 5) > 3 && x == 5 changes x and reads it, but && fixes the order, so the answer to question 3 is yes. i++ + i++ changes i twice with no order between the changes, so it fails question 3. This check is short, and it takes less time than running the program to see what happens.
The case i = i++
The line i = i++ is the most famous example. A reader expects the value of i to go up by one. The next program shows what happens.
#include <iostream>
using namespace std;
int main() {
int i = 5;
i = i++;
cout << i << endl;
return 0;
}The output on the machine that checked this lesson with -std=c++20 is:
5The value does not change. The increment i++ runs first and sets i to 6, but it returns the old value, 5. Then the assignment writes that old value back into i, so i is 5 again. The increment was lost.
Here is what the compiler reported when it checked the same line, with -Wall turned on:
ex_i.cpp: In function ‘int main()’:
ex_i.cpp:6:7: warning: operation on ‘i’ may be undefined [-Wsequence-point]
6 | i = i++;
| ~~^~~~~
ex_i.cpp:6:7: warning: operation on ‘i’ may be undefined [-Wsequence-point]
5The warning says the operation may be undefined. Before C++17, that was the correct reading: the line changed i twice with no order between the two changes, so the result was undefined. Compiled with -std=c++14, this program gives the same 5 and the same warnings, but the standard then gave no guarantee. The rule that makes it defined in C++17 is subtle, and g++ still treats it as suspicious. Do not write i = i++. If you want to add one, write i++; on its own line, or i += 1;.
Why the standard leaves some orders open
If the standard fixed every order, the rules would be easier to learn. It leaves some orders open for a practical reason. A compiler may compute the operands of + in whichever order fits the registers of the processor, or whichever order makes the instructions shorter. Fixing one order for every case would force the compiler to add extra work in some cases.
The cost is that code which depends on the open order can behave differently between compilers, between versions, or between two builds with different options. The language still gives you one firm rule that covers almost every real case. If two steps change the same variable, or one changes it while the other reads it, and nothing forces an order between them, the program has undefined behaviour.
Common mistakes
Mistake 1: changing the same variable twice
Here is the expression that most interview questions use.
#include <iostream>
int main() {
int i = 3;
int x = i++ + i++;
return x;
}The compiler reports this with the same kind of warning as before:
ex_x.cpp: In function ‘int main()’:
ex_x.cpp:4:14: warning: operation on ‘i’ may be undefined [-Wsequence-point]
4 | int x = i++ + i++;
| ~^~Both i++ steps change i. The + operator has no fixed order between its two operands, so there is no order between the two changes either. The result is undefined. The program might return 7, or it might return something else. The fix is to make each change its own statement.
int i = 3;
int first = i++;
int second = i++;
int x = first + second;Now first is 3, second is 4, and x is 7. Each statement changes i once, and the statements run in order.
Mistake 2: relying on the order of function arguments
The first program in this lesson showed that show_pair(next_id(counter), next_id(counter)) printed first = 2, second = 1 with g++. A reader who expects first = 1 will be wrong on that compiler, and may be right on another. The fix is to call the function first and store the results in variables, one per line.
int counter = 0;
int first = next_id(counter);
int second = next_id(counter);
show_pair(first, second);This prints first = 1, second = 2 on every compiler, because each call is a separate statement.
Mistake 3: reading the warning as optional
A warning such as operation on 'i' may be undefined is not a style note. It means the compiler found a line that the standard does not define. Turn on -Wall when you compile, and treat each warning of this kind as a bug to fix. The check costs nothing and catches the errors in this lesson before they reach a test.
Practice
Try each exercise on paper first, then run your answer to check it.
Exercise 1. What does this program print?
int i = 2;
int j = i++ * 10;
cout << i << " " << j;Show the answer
It prints 3 20. The increment runs once, so i becomes 3. The old value 2 is multiplied by 10, which gives j = 20.
Exercise 2. Does C++ guarantee that f() + g() calls f first?
Show the answer
No. The operands of + have no fixed order, so g may run first. If f and g print something, the printed order can differ between compilers. If you need f first, store its result in a variable on its own line, then call g.
Exercise 3. The line x = a++ + a++; has undefined behaviour. Rewrite it with one change to a per statement, so that a starts at 1 and the final x is the sum of the old values of the two increments. What are the final values of x and a?
Show the answer
int x = a++;
x += a++;The first line sets x to 1 and a to 2. The second line adds the old value of the second increment, which is 2, so x becomes 3. The final values are x = 3 and a = 3.
Exercise 4. A function next_id(int& counter) increments counter and returns the new value. In C++17 and later, what does cout << next_id(c) << " " << next_id(c); print when c starts at 0?
Show the answer
It prints 1 2. Since C++17, the operands of << in a chain are evaluated from left to right, so the first call runs before the second.
Exercise 5. With e equal to 1, what does int r = (e += 2, e * 10); store in r, and why is it not 10?
Show the answer
It stores 30. The comma operator runs its left part first, so e += 2 makes e equal to 3. Then e * 10 runs and gives 30. If the right part ran first, e would still be 1 and the result would be 10, but the comma operator does not work that way.
Summary
- Precedence decides how operands group. Evaluation order decides which operand runs first. They are separate rules.
- The operands of
+and the arguments of a function call have no fixed order, so do not depend on which runs first. - Since C++17 the operands of
<<in a chain run from left to right, and the right side of=runs before the left side. i = i++does not add one toiin practice. The increment is overwritten, and g++ warns about the line.- If one expression changes the same variable twice with no order between the changes, the result is undefined. Split the expression into statements.
- Turn on
-Walland treat eachmay be undefinedwarning as a bug.