Programming Languages · Language Design

Evaluation Order, Sequencing Points, and Unspecified Behaviour

Why C leaves the order of operand evaluation open, which operators impose sequencing, and how a language that fixes left-to-right evaluation pays for the guarantee.

The expression f() + g() has a value, and if both functions are pure that value is the same whichever runs first. If either has a side effect, the order matters. A language specification has to say something about that order, and the three plausible answers are: fix it, leave it unspecified, or leave it undefined. C chooses unspecified for most operators; Java chooses fixed left-to-right; C++ fixes some and leaves others open while adding a sequencing relation that constrains the compiler without naming an order.

The difference between unspecified and undefined

These two words are not synonyms in a specification, and the distinction is what makes C’s choice defensible.

Unspecified means the implementation may choose any behaviour from a set of permitted ones, and it must choose consistently for a given program. The program has a value; it may differ between compilers. f() + g() with printing side effects produces one of two output orders, both valid. A conforming program cannot depend on which, but it is not erroneous.

Undefined means the specification imposes no requirement at all. The program is not a conforming program, and the compiler may do anything. An unsequenced read and write of the same object, or two unsequenced writes, falls here: in C, i = i++ + 1 is undefined, not merely unspecified, because the modification of i and one of the reads of it are unsequenced. The distinction matters because a compiler may assume undefined behaviour never occurs, and optimisations built on that assumption are legitimate.

C’s expression rules are mostly unspecified, with a small set of undefined cases where unsequenced accesses collide. The design constraint is optimisation: a fixed order forces the compiler to preserve the sequence of every operand’s side effects, which blocks reordering, common subexpression motion, and register allocation choices. Leaving the order open lets the compiler schedule operands to fit the target.

Where C does impose an order

C’s sequencing points are few and specific. The && and || operators evaluate the left operand first and evaluate the right only if needed, so short-circuiting is a sequencing guarantee, not an optimisation. The conditional operator ?: evaluates its condition first and then exactly one branch. The comma operator sequences left before right. A full expression ends at a sequence point, so statements are ordered. A function call has a sequence point after the evaluation of the function designator and arguments and before the call itself, which orders the call relative to the argument evaluations as a group but not the arguments relative to each other.

Everything else is open. In a[i] = i++, the subscript and the increment are unsequenced, so the program is undefined if i is the same object. In printf("%d %d", i++, i++), the two argument evaluations are unsequenced, so the program is undefined. The justification for the rule is that no order is more correct than any other, and picking one would forbid an optimisation that the source does not require.

The rule that ties this together is the sequenced-before relation. Two evaluations are sequenced if one happens before the other by the rules; otherwise they are unsequenced. An unsequenced pair where one modifies an object and the other reads or modifies the same object is undefined. This formulation is what makes the rule checkable: it does not require knowing an order, only knowing whether an order is guaranteed.

Java’s fixed order

Java takes the opposite position: every operand is evaluated left to right, including array references, method receivers, and arguments. The language specification states the order as part of the language, and the guarantee is observable.

The guarantee costs the implementation freedom. A Java compiler cannot reorder f() + g() even when both are pure, because it cannot in general prove purity, and the specification gives it no licence to try. It also constrains exception behaviour: if f() throws, g() must not have run, and any side effect g() would have produced must be absent. That is a semantic guarantee a programmer can rely on, and it is why Java code can depend on the order of logging calls inside an argument list.

The cost is contained because the compiler still has freedom inside a single operand’s evaluation, and because the guarantee only constrains code that has observable side effects in operand position. The benefit is that the language has no undefined-behaviour class corresponding to unsequenced access; i = i++ + 1 in Java has a defined result, computed by evaluating the right side, then assigning.

C# makes the same choice, with a documented left-to-right order for operands. Python fixes left-to-right evaluation of call arguments, and defines most binary operators as evaluating the left operand first without guaranteeing that the right operand is reached in every implementation. Go fixes the order of function calls and assignments in its specification, while leaving the order of some operand evaluations within an expression to the implementation, a middle position that is documented rather than accidental.

What C++ added

C++ inherited C’s unspecified order but added a formal relation: every expression has a sequenced-before relation, and the rules define which subexpressions are sequenced. The relation no longer fixes every operator the same way. The left operand of the shift operators is sequenced before the right, the right operand of an assignment is sequenced before the left, the condition of ?: is sequenced before the selected branch, and the initialiser of each init-declarator is sequenced before the declarator that follows it in the same declaration. Arithmetic operators are still left open. The relation does not name an evaluation order in the machine-code sense; it constrains which orderings are legal, so that a compiler may interleave but may not produce an observable reordering that violates the relation.

The practical effect is that far less code is undefined than in C. i = i++ + 1 is undefined in C11 and later, because the increment’s side effect is not sequenced with respect to the assignment; C++17 moved further and defines it, and defines i = i++ + i as undefined. f() + g() where both functions have side effects is now ordered if the operator sequences its operands. C++ also introduced the rule that a function call’s argument evaluations are indeterminately sequenced with respect to each other, which means they may not interleave but their relative order is unspecified. That closes a class of bugs where two arguments each modified a shared object and their effects overlapped.

Indeterminately sequenced is a useful middle term to know: it forbids interleaving without fixing an order. A compiler may evaluate arguments in either order, but must complete one before starting the next. The guarantee is weaker than Java’s and stronger than C’s, and it is expressible only because the specification introduced the vocabulary.

Designing the rule

A language designer picking an evaluation order is choosing among four positions, and the choice propagates into error messages, debugging, and optimisation.

Fixing left-to-right gives a simple mental model and predictable side effects. It makes the order part of the language’s observable behaviour, so any change is a breaking change, and it constrains optimisations that reorder effectful operands. It also makes the order of exception propagation predictable, which is a real benefit in code that relies on cleanup.

Leaving it unspecified maximises optimisation freedom and minimises the specification’s obligations. It requires programmers to avoid side effects in operand position, a discipline that is easy to state and hard to enforce, and it means a program’s output can change when the compiler changes. The cost lands on anyone debugging: a test that passes with one compiler version can print in a different order with the next.

Defining a sequencing relation without fixing an order, the C++ route, gets most of the safety with most of the freedom, at the price of a more complex specification and a rule that programmers cannot easily apply by inspection. The relation is checkable by a compiler but not obvious to a reader, so the diagnostic quality depends on the tool.

Leaving it undefined, as C does for unsequenced access to the same object, is the harshest position. It permits aggressive optimisation but makes some ordinary-looking expressions erroneous. The justification is that the erroneous expressions are almost always mistakes, and that detecting them statically is possible, which is why compilers offer warnings for the unsequenced-access pattern.

Where the order becomes visible

Three situations make evaluation order observable even in languages that fix it. The first is side effects in argument lists, where the order determines what a log shows. The second is exceptions, where the order determines which cleanup runs and which exception propagates when two operands both throw. The third is resource acquisition, where open(a) + open(b) leaves one descriptor open if the second call throws before the first is stored anywhere.

The third case is the reason some languages guarantee that each operand is fully evaluated before the next begins, which bounds how much remains to be unwound when one throws, and why some style guides forbid resource acquisition in argument position entirely. A language that fixes left-to-right makes the failure order predictable; a language that leaves it unspecified makes which descriptor leaks depend on the compiler.

The same concern appears in a different form in Short-Circuit Evaluation as a Language Guarantee, where the ordering is not merely permitted but is the mechanism the operator exists to provide. Short-circuiting is the one place where C’s otherwise unspecified order becomes a hard guarantee, and the reason is that the operator would be useless without it.

Applying the rule to a given expression

For a given expression, a programmer can ask three questions. Is there a sequence point or a sequenced-before edge between these two evaluations? If not, do both touch the same object with at least one write? If yes, the program is undefined and the compiler may assume it does not occur. If the evaluations are ordered but the order is not fixed by the specification, the program is unspecified and may produce different results on different implementations.

That decision procedure is the whole of the rule, and it explains why the answer differs between C, C++, and Java without any of them being inconsistent. The languages disagree about how many edges the relation has, and the number of edges is a design choice that trades optimisation freedom against predictability.

One operator turns the ordering guarantee into the point of the construct rather than a side condition. Short-Circuit Evaluation as a Language Guarantee covers the case where the right operand must not run at all, which no amount of compiler freedom can override.

References

  1. ISO/IEC 9899 — Programming languages — C, 6.5 Expressions — ISO/IEC
  2. The Java Language Specification — 15.7 Evaluation Order — Oracle
  3. C++ standard draft, [intro.execution] — Sequencing — ISO/IEC 14882 draft