Evaluation Order
Rules governing when operands, subexpressions, and side effects are evaluated within a C++ expression, and which orderings produce undefined behavior.
Evaluation Ordersince C++11The order in which operands, subexpressions, and their side effects are evaluated is governed by the sequenced-before relation; evaluations without a sequenced-before relationship are unsequenced, and any side effect on a memory location that is unsequenced relative to another side effect or value computation on the same location is undefined behavior.
Overview
Operator precedence and associativity determine grouping β which operands belong to which operator β but they say nothing about when those operands are computed at runtime. The two concepts are orthogonal. a + b * c means a + (b * c), but whether a, b, or c is computed first is unspecified.
Pre-C++11 described ordering boundaries using sequence points: positions in execution at which all prior side effects were guaranteed complete. C++11 replaced this model with the more expressive sequenced-before relation: an asymmetric, transitive ordering between individual evaluations within a thread.
For any two evaluations A and B:
- A sequenced before B β A completes entirely before B begins.
- A indeterminately sequenced with B β one completes before the other, but the order is unspecified and may vary; they cannot overlap.
- A and B unsequenced β may execute in any order, and may be interleaved. A side effect unsequenced relative to another side effect or a read of the same memory location is undefined behavior.
Evaluating an expression involves both its value computation (producing a result or object identity) and any side effects (object modification, I/O, volatile access). Both are subject to sequencing rules independently.
Sequencing Guarantees
The following constructs impose sequenced-before relationships:
Comma operator β left is sequenced before right:
int x = (a = 2, a + 1); // a=2 completes first; x is 3&& and || β left operand sequenced before right, with short-circuit evaluation:
while (p && !whitespace(*p)) // p checked before *p is dereferenced
++p;Conditional ?: β condition is sequenced before the selected branch; the other branch is not evaluated:
int v = flag ? f() : g(); // exactly one of f or g is calledBraced-init-lists (C++11) β each initializer clause is sequenced before subsequent ones:
// C++11: f, then g, then h β guaranteed
std::vector<int> data = {f(), g(), h()};Assignment and compound assignment (C++17) β the right-hand operand is fully sequenced before the left-hand operand:
int v[5] = {}, i = 1;
v[i] = i++; // C++17: i++ evaluates first (value=1, then i=2), then v[2]=1
// C++14 and earlier: undefined behaviorSubscript, member access, and shift operators (C++17) β E1 is sequenced before E2 in E1[E2], E1.E2, E1.*E2, E1->*E2, E1 << E2, E1 >> E2:
// C++17: left-to-right evaluation of member chains is guaranteed
std::string s = "abcde";
s.replace(0, 1, "A").replace(2, 1, "C"); // well-defined order in C++17Function calls β the function body begins only after all argument expressions are evaluated. The postfix expression designating the function is sequenced before all arguments since C++17. However, the order among arguments is unspecified in all standards.
Examples
Argument evaluation order β unspecified in all standards
int i = 0;
// Undefined behavior: f and g both read/write i, but their evaluation is unsequenced
printf("%d %d\n", f(i), g(i++));
// Even without UB, the printed values depend on which runs first
int counter = 0;
auto next = [&]{ return counter++; };
printf("%d %d\n", next(), next()); // may print "0 1" or "1 0"Fix: sequence the calls explicitly.
int a = next();
int b = next();
printf("%d %d\n", a, b); // well-definedClassic UB: multiple modifications without sequencing
int i = 0;
i = i++; // UB pre-C++17; well-defined in C++17 (i ends as 0)
i++ + i++; // UB in all standards β no sequencing between the two increments
int v[2] = {};
v[i++] = i++; // UB in all standards β both sides modify/read i without sequencingi++ + i++ remains undefined in C++17 because arithmetic operators still impose no sequencing between their operands.
std::cout chaining
// C++14 and earlier: UB if a or b has side effects on a shared variable
std::cout << a() << b();
// C++17: E1 << E2 sequences E1 before E2, so a() runs before b()
// Each << is left-to-right sequenced in C++17Overloaded operators lose sequencing
The sequencing guarantees on &&, ||, and , apply only to the built-in forms:
// Built-in comma: a() is fully evaluated before b()
int r = (a(), b());
// Overloaded comma: no such guarantee
MyType result = (obj1, obj2); // order of obj1 and obj2 is unspecifiedThis is a known hazard in expression-template libraries.
Best Practices
Never modify and read the same variable in the same expression unless a sequencing operator (&&, ||, ,, ?:, or built-in <</>> in C++17) sits between them. When in doubt, split into separate statements.
// Problematic intent β reads and modifies i in one expression
result = arr[i++]; // fine: one read of i, one increment β no conflict
result = arr[i] + i++; // UB: arr[i] and i++ are unsequenced reads/writes of iSequence explicit dependencies in function arguments:
// Risky: relies on unspecified argument order
log(allocate_id(), allocate_id());
// Clear:
auto id1 = allocate_id();
auto id2 = allocate_id();
log(id1, id2);Compile with sanitizers. UBSan (-fsanitize=undefined) catches unsequenced modification at runtime. Clang's -Wsequence-point and GCC's equivalent warn on common patterns statically.
Target C++17 when relying on left-to-right member-chain evaluation. Builder APIs and streaming operators became reliably left-to-right in C++17; mark the minimum standard in your build system.
Common Pitfalls
Conflating precedence with evaluation order. f(i) * g(i++) computes f(i) and g(i++) with unspecified order before multiplying, regardless of what precedence says about *.
Assuming left-to-right flow. The only built-in operators that guarantee left-to-right evaluation are &&, ||, ,, ?:, and (since C++17) <<, >>, subscript, and member-access chains. Everything else β including +, -, *, and function arguments β has unspecified order.
Forgetting that overloaded operators lose sequencing. Overloaded && and || don't short-circuit, and overloaded , doesn't sequence. Code that depends on these guarantees must use the built-in forms or explicit statements.
Mixing new-expression arguments. In a new-expression, the allocation function call and the constructor arguments are indeterminately sequenced with respect to each other. If constructor arguments have side effects, their interaction with the allocator is unspecified.
Formal Sequencing Model
C++11 replaced the pre-C++11 sequence points concept with a richer relation. For any two evaluations A and B within a single thread, exactly one of three cases holds:
| Relationship | What it means | Side-effect safety |
|---|---|---|
| A sequenced before B | A completes entirely before B begins; asymmetric and transitive | Safe: no ordering conflict |
| A indeterminately sequenced with B | One completes before the other, but which is unspecified and may differ between executions; they cannot overlap | Safe: no overlap, but result is nondeterministic |
| A and B unsequenced | May execute in any order and may interleave in a single thread (the compiler may interleave CPU instructions) | Unsafe: a side effect on the same object unsequenced with another side effect or a read of that object is undefined behavior |
"Sequenced after" is simply the inverse relation: B sequenced after A means A sequenced before B.
Both the value computation (producing a result) and any side effects (object modification, I/O, volatile access) of an expression are subject to sequencing rules independently. Rule 2 from the standard states that value computations of operands are sequenced before the value computation of the operator's result, but this does not constrain the side effects of those operands relative to each other.
C++17 Rule Changes in Full
C++17 (paper P0145R3) added the following new sequencing guarantees. In each case, a is sequenced before b:
| Expression form | Rule | Standard reference |
|---|---|---|
a.b | a before b | [expr.ref] |
a->b | a before b | [expr.ref] |
a->*b | a before b | [expr.mptr.oper] |
a[b] | a before b | [expr.sub] Β§16 |
a << b | a before b | [expr.shift] Β§18 |
a >> b | a before b | [expr.shift] Β§18 |
a @= b (any assignment) | b before a (right before left) | [expr.ass] Β§19 |
f(b1, b2, b3) β the function designator | f before each bi | [expr.call] Β§13 |
f(b1, b2, b3) β among arguments | b1, b2, b3 are indeterminately sequenced with each other | [expr.call] Β§14 |
The distinction in the last two rows is critical: since C++17, the postfix expression naming the function (f) is sequenced before all arguments, but the arguments are still only indeterminately sequenced with respect to one another. The compiler cannot interleave argument sub-expressions (e.g. it cannot begin evaluating b2 partway through b1), but it may still evaluate b3 before b1.
For overloaded operators called with operator notation (a << b), rule 15 ensures the same sequencing as the corresponding built-in operator applies. This is what makes std::cout << f() << g() left-to-right in C++17.
The operator<< chain explained
std::cout << f() << g() << h();This parses (due to left-associativity) as:
operator<<(operator<<(operator<<(std::cout, f()), g()), h());Before C++17: f(), g(), and h() could be evaluated in any order β all six permutations were legal. If any of them had side effects on a shared variable, the result was implementation-defined (or worse, undefined behavior if they modified the same object).
C++17 and later: Rule 18 (E1 << E2 sequences E1 before E2) applies at each level. The outermost << sequences its left operand (the result of the middle <<) before h(). The middle << sequences its left operand (the result of the innermost <<) before g(). And the innermost << sequences std::cout before f(). This guarantees f() runs first, then g(), then h() β left-to-right.
int x = 0;
auto side = [&](const char* name) -> int {
std::cout << name << ' ';
return ++x;
};
// C++14: order of side(), side(), side() calls is unspecified
// C++17: guaranteed to print "f g h" followed by values 1 2 3
std::cout << side("f") << ' ' << side("g") << ' ' << side("h") << '\n';Member-chain sequencing
Before C++17, chained member calls like the Stroustrup example from The C++ Programming Language 4th ed. produced unspecified results:
std::string s = "but I have heard it works even if you don't believe in it";
s.replace(0, 4, "")
.replace(s.find("even"), 4, "only")
.replace(s.find("don't"), 6, "");
// C++14: unspecified β s.find() calls may run before the preceding replace()
// C++17: left-to-right; result is "I have heard it works only if you believe in it"Because a.b sequences a before b (rule 16 in cppreference, covering E1[E2], and the a.b form), the chain s.replace(...).replace(s.find(...), ...) now guarantees that the first replace completes β and modifies s β before the second s.find runs.
Function argument interleaving: the make_unique case
Before C++17, this code had a subtle memory-safety issue:
// C++14: new T may run, then otherFunction() may throw,
// then unique_ptr constructor never runs β memory leaked
foo(std::unique_ptr<T>(new T), otherFunction());The problem: the compiler could interleave sub-expressions of different arguments. It could execute new T, then call otherFunction() (which throws), then never reach the unique_ptr constructor β leaking the raw pointer.
C++17 fix: argument sub-expressions may not interleave. If the compiler begins evaluating std::unique_ptr<T>(new T), it must finish that entire sub-expression before starting otherFunction(). This eliminates the specific leak above.
What C++17 does not fix: the order among arguments remains unspecified. The compiler is still free to evaluate otherFunction() entirely before new T β which means a different throw scenario could still leak. std::make_unique<T>() remains the correct idiom because it fuses allocation and ownership transfer into a single indivisible call.
// Correct in all standards β allocation and unique_ptr construction are atomic
foo(std::make_unique<T>(), otherFunction());Quick-reference: what sequences left-to-right since C++17
Operator/form Order guaranteed (C++17+)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
a.b a->b a->*b a before b
a[b] a before b
a << b a >> b a before b (makes cout chains safe)
a = b a @= b b (right) before a (left)
f(b1, b2, b3) β designator f before any bi
f(b1, b2, b3) β arguments indeterminately sequenced (no interleave, order unspecified)
{a, b, c} braced-init-list left-to-right (C++11, unchanged)
a && b a || b a,b a?b:c left before right (all standards)
Still unspecified in C++17 and later:
a + b a - b a * b a / b (and all other arithmetic/bitwise ops)
function argument orderFunction argument evaluation order
Arguments to a function are indeterminately sequenced with each other in every version of C++. The compiler is free to evaluate them in any order, and may differ between builds, optimization levels, or target architectures. There is no standard left-to-right guarantee.
// C++17 β arguments are indeterminately sequenced; may print "0 1" or "1 0"
int counter = 0;
auto next = [&]{ return counter++; };
printf("%d %d\n", next(), next());When side-effect-bearing arguments share state, sequence them manually:
// Safe: explicit ordering
auto a = next();
auto b = next();
printf("%d %d\n", a, b); // always "0 1"Since C++17, the function designator is sequenced before all arguments, but arguments among themselves remain indeterminately sequenced β they cannot interleave mid-evaluation (eliminating the classic unique_ptr(new T) / throwing-argument leak), but their start-to-finish order is still unspecified. Use std::make_unique and similar factory functions to eliminate ordering sensitivity entirely.
// C++14: new T, then otherFn() may throw before unique_ptr ctor β leak possible
foo(std::unique_ptr<T>(new T), otherFn()); // C++14
// C++17+: sub-expressions cannot interleave, but otherFn() may still run first
foo(std::unique_ptr<T>(new T), otherFn()); // C++17
// Correct in all standards: allocation and ownership in one indivisible step
foo(std::make_unique<T>(), otherFn()); // C++11Operator chaining and left-to-right rules
Left-associativity of an operator means chained uses group from left to right, but does not by itself impose any evaluation order. Only the specific operators listed in the sequencing rules above guarantee that the left operand is evaluated before the right.
For arithmetic operators, grouping and execution order are independent:
int a = f() + g() + h();
// Groups as (f() + g()) + h() β but f, g, h may run in any order
// C++17 does not change this: + still has no sequencing guaranteeSince C++17, << and >> do impose sequencing, which is what makes std::cout chains safe:
// C++17: guaranteed left-to-right β f() then g() then h()
std::cout << f() << g() << h() << '\n'; // C++17
// C++14 and earlier: f(), g(), h() may run in any order β undefined if they share state
std::cout << f() << g() << h() << '\n'; // C++14Member-access chains (., ->, ->*) also gained left-to-right guarantees in C++17, fixing builder-pattern and fluent-interface idioms:
// C++17: each .method() runs fully before the next argument is evaluated
builder.setName("Alice")
.setAge(30)
.setRole("admin"); // C++17: guaranteed left-to-rightShort-circuit evaluation
The && and || operators guarantee left-to-right evaluation and stop as soon as the result is determined. This guarantee applies to their built-in forms only; overloaded operator&& and operator|| do not short-circuit.
// Safe: p is checked before *p is dereferenced
char* p = getPtr();
while (p && *p != '\0') // if p is null, *p is never evaluated
++p;
// Safe: f() only called when ptr is non-null
if (ptr != nullptr && ptr->isValid())
ptr->process();The right operand is not evaluated if the left operand already decides the outcome: false && expr skips expr; true || expr skips expr. This is essential for null checks, range guards, and lazy initialization.
// Short-circuit prevents division by zero
int x = 0;
if (x != 0 && 100 / x > 2) // 100/x never evaluated when x == 0
handle();
// || stops at first true
bool ok = tryFast() || trySlow() || tryFallback();
// trySlow() is only called if tryFast() returns false
// tryFallback() is only called if both tryFast() and trySlow() return falseOverloaded && and || lose short-circuit behavior. They are called as ordinary functions, so both operands are fully evaluated before the call:
// C++11: MyBool operator&&(MyBool a, MyBool b) β BOTH a and b are evaluated
MyBool result = canFail() && mightThrow(); // mightThrow() always runsOrder of initialization of static variables
Static-duration variables at block scope are initialized the first time control passes through their declaration; this is guaranteed thread-safe since C++11. Static variables at namespace scope within a translation unit are initialized in declaration order, but the order between translation units is unspecified β this is the static initialization order fiasco.
// C++11: thread-safe lazy initialization β only one thread initializes
static MyRegistry& getRegistry() {
static MyRegistry instance; // initialized once, on first call
return instance;
}Variables with constant initialization are initialized before any dynamic initialization occurs:
// Constant initialization β no order dependency, always safe
constexpr int kMax = 100; // C++11: zero/constant init, no runtime cost
inline constexpr double kPi = 3.14159265358979; // C++17: inline constexprThe fiasco arises when one translation unit's dynamic initializer depends on a variable from another TU:
// file_a.cpp
int base = 10; // dynamic initialization order between TUs is unspecified
// file_b.cpp
extern int base;
int derived = base * 2; // may see base == 0 if file_b initializes firstFix with the construct-on-first-use idiom:
// Returns a reference; local static is initialized on first call (C++11: thread-safe)
int& getBase() {
static int value = 10;
return value;
}
int derived = getBase() * 2; // safe: getBase() forces initialization before useConstant expressions and evaluation order
Constant expressions are evaluated at compile time, so "evaluation order" refers to the sequencing rules of the C++ abstract machine applied during constant evaluation. The same sequenced-before rules apply; undefined behavior in a constexpr context is a compile error rather than a runtime hazard.
// C++11: constexpr function β sequencing is enforced at compile time
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
static_assert(factorial(5) == 120);
// C++14: relaxed constexpr β local variables, loops, and sequencing all work
constexpr int sum(int n) {
int total = 0;
for (int i = 1; i <= n; ++i)
total += i;
return total; // C++14
}
static_assert(sum(10) == 55);In constexpr evaluation, an expression that would be UB at runtime causes the constant expression to be ill-formed:
// C++14: ill-formed if called at compile time β compiler diagnoses the UB
constexpr int badShift(int x) {
return x << 40; // UB: shift count >= width of int β compile error in constexpr
}if constexpr (C++17) evaluates the condition at compile time and discards the untaken branch, guaranteeing no evaluation of the discarded side:
// C++17: the else branch is not instantiated when T is integral
template<typename T>
auto describe(T v) {
if constexpr (std::is_integral_v<T>) // C++17
return "integer: " + std::to_string(v);
else
return std::string("other");
}Detecting evaluation order bugs with sanitizers
Undefined-behavior sanitizers (UBSan) and static analyzers can catch many β but not all β evaluation-order bugs at development time. The key tools are -fsanitize=undefined for runtime detection and compiler warning flags for static detection.
Compile flags for evaluation-order checking:
// Compiler invocation (not C++ code)
// GCC and Clang: runtime UB detection
// -fsanitize=undefined catches signed overflow, invalid shifts, etc.
// -Wsequence-point warns on classic sequence-point violations (GCC/Clang)
// Clang-specific:
// -fsanitize=undefined,address combined sanitizer run
// clang-tidy with bugprone-* checks covers some argument-order issuesA minimal example that UBSan will catch at runtime:
// C++11: i++ + i++ β both increments are unsequenced; UBSan reports "store to address X
// that overlaps with store to address X"
#include <cstdio>
int main() {
int i = 0;
int r = i++ + i++; // UB: unsequenced modification of i
printf("%d\n", r);
}GCC's -Wsequence-point flag will warn on this pattern even without sanitizers. Clang's -Wunsequenced flag covers the same class of issues:
int i = 0;
int r = i++ + i++; // warning: unsequenced modification and access to 'i' [-Wunsequenced]Sanitizers only catch bugs that are exercised at runtime. Patterns involving unspecified argument evaluation order β where both orderings happen to produce the same result for a particular input β will not trigger UBSan. The only reliable defense is code review, compiler warnings, and writing evaluation-order-safe code by design.
See Also
reference/language/aggregate-initializationβ braced-init-lists sequence initializers left to right since C++11