Skip to content
C++
Language
Advanced

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++11

The 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:

cpp
int x = (a = 2, a + 1);  // a=2 completes first; x is 3

&& and || β€” left operand sequenced before right, with short-circuit evaluation:

cpp
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:

cpp
int v = flag ? f() : g();  // exactly one of f or g is called

Braced-init-lists (C++11) β€” each initializer clause is sequenced before subsequent ones:

cpp
// 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:

cpp
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 behavior

Subscript, 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:

cpp
// 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++17

Function 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

cpp
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.

cpp
int a = next();
int b = next();
printf("%d %d\n", a, b);  // well-defined

Classic UB: multiple modifications without sequencing

cpp
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 sequencing

i++ + i++ remains undefined in C++17 because arithmetic operators still impose no sequencing between their operands.

std::cout chaining

cpp
// 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++17

Overloaded operators lose sequencing

The sequencing guarantees on &&, ||, and , apply only to the built-in forms:

cpp
// 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 unspecified

This 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.

cpp
// 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 i

Sequence explicit dependencies in function arguments:

cpp
// 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:

RelationshipWhat it meansSide-effect safety
A sequenced before BA completes entirely before B begins; asymmetric and transitiveSafe: no ordering conflict
A indeterminately sequenced with BOne completes before the other, but which is unspecified and may differ between executions; they cannot overlapSafe: no overlap, but result is nondeterministic
A and B unsequencedMay 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 formRuleStandard reference
a.ba before b[expr.ref]
a->ba before b[expr.ref]
a->*ba before b[expr.mptr.oper]
a[b]a before b[expr.sub] Β§16
a << ba before b[expr.shift] Β§18
a >> ba before b[expr.shift] Β§18
a @= b (any assignment)b before a (right before left)[expr.ass] Β§19
f(b1, b2, b3) β€” the function designatorf before each bi[expr.call] Β§13
f(b1, b2, b3) β€” among argumentsb1, 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

cpp
std::cout << f() << g() << h();

This parses (due to left-associativity) as:

cpp
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.

cpp
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:

cpp
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:

cpp
// 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.

cpp
// 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

cpp
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 order

Function 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.

cpp
// 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:

cpp
// 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.

cpp
// 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++11

Operator 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:

cpp
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 guarantee

Since C++17, << and >> do impose sequencing, which is what makes std::cout chains safe:

cpp
// 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++14

Member-access chains (., ->, ->*) also gained left-to-right guarantees in C++17, fixing builder-pattern and fluent-interface idioms:

cpp
// C++17: each .method() runs fully before the next argument is evaluated
builder.setName("Alice")
       .setAge(30)
       .setRole("admin");  // C++17: guaranteed left-to-right

Short-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.

cpp
// 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.

cpp
// 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 false

Overloaded && and || lose short-circuit behavior. They are called as ordinary functions, so both operands are fully evaluated before the call:

cpp
// C++11: MyBool operator&&(MyBool a, MyBool b) β€” BOTH a and b are evaluated
MyBool result = canFail() && mightThrow();  // mightThrow() always runs

Order 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.

cpp
// 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:

cpp
// 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 constexpr

The fiasco arises when one translation unit's dynamic initializer depends on a variable from another TU:

cpp
// 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 first

Fix with the construct-on-first-use idiom:

cpp
// 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 use

Constant 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.

cpp
// 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:

cpp
// 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:

cpp
// 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:

cpp
// 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 issues

A minimal example that UBSan will catch at runtime:

cpp
// 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:

cpp
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