In this post we review the rules concerning the order of evaluation of expressions in c++14 and c++17. The sequenced-before partial order binary relation is introduced and copious references to the standard are given. Sequenced-before graphs are then introduced and used to determine the well-definedness (or undefinedness) of example expressions.

Definitions

The « standard » means the draft n4659 unless mentioned otherwise. This draft should be very close to the final c++17 standard. (See this question on stackoverflow) We will indicate which rules apply only to c++17 and which rules apply to both c++17 and c++14. For the precise wording of the rules and definitions the standard should be consulted.

We first need some definitions. Skip them if you are already familiar with them:

  • observable behaviours — Only the observable behaviours are required to be emulated by a conforming implementation. This rule is sometimes called the as-if rule since the implementation can do whatever it wants as long as from the point of view of the observable behaviours the program behaves as written (4.6 [intro.execution]/1). The observable behaviours are (4.6 [intro.exection]/7):
    • accesses to volatile glvalues
    • data written to files at program termination
    • during input/output, prompting output must be delivered before waiting for input
  • side effects — The following are defined to be side effects (4.6 [intro.execution]/14):
    • reading a volatile glvalue
    • modifying an object
    • calling a library I/O function
    • calling a function which does any of the above
  • value/side effects computation — An evaluation of an expression involes in general both a value computation (the result of the evaluation of the expression) and a side effect computation (the side effects of the evaluation of the expression) (4.6 [intro.execution]/14). Given an expression , we will abbreviate the value computation of by val and the side effect computation of by se .

  • partial order — A partial order on a set is a binary relation on that is:
    • transitive: , and
    • antisymmetric: , and
    • reflexive: ,
  • strict partial order — A strict partial order on a set is a binary relation on that is:
    • transitive (see above)
    • irreflexive: such that

    A partial order on a set is equivalent to a strict partial order on by defining and .

  • the sequenced-before strict partial order — The standard defines (4.6 [intro.execution]/15) the sequenced-before strict partial order on the set of value/side effect computations in a single thread by occurs before , where and are two value/side effect computations. As a notation simplification, given two expressions and we will write and say that is sequenced before to mean « all value and side effect computations, recursively, of are sequenced before all value and side effect computations, recursively, of  ».

  • unsequenced/indeterminately sequenced — Given two value/side effect computations and , and are said to be unsequenced if neither nor . and are said to be indeterminately sequenced if either or , but which one is unspecified (4.6 [intro.execution]/15).

  • well-defined/implementation-defined/unspecified/undefined behaviors — Behaviors can fall into four categories defined in (4.6 [intro.execution]/1-5):
    • well-defined — The behavior is defined by the standard.
    • implementation-defined — The standard specifies a range of possible behaviors and the implementation must document what is the actual behavior.
    • unspecified — The standard specifies a range of possible behaviors but the actual behavior in this range is not specified and might vary from one execution of a program to another.
    • undefined — The standard imposes absolutely no requirements on the behavior of programs which contain undefined behaviors. In particular the program might do anything between work for some sense of the word «work», crash, crash your computer and launch all missiles. More realistically the compiler can assume that the undefined behaviors do not occur which will give rise to surprising behaviors.
  • memory location — A memory location is one of the following (4.4 [intro.memory]/3):
    • object of scalar type (which are possibly cv-qualified arithmetic types, enumeration types, pointer types, pointer-to-member types and std::nullptr_t (6.9 [basic.types]/9))
    • maximal sequence of adjacent bitfields all having non-zero width

Well-definedness criterion

We can now give the criterion for the well-definedness of an expression. The behavior is well-defined unless one of the following is true, in which case the behavior is undefined (4.6 [intro.execution]/17):

  • A side effect on a memory location is unsequenced with another side effect on the same memory location
  • A side effect on a memory location is unsequenced with a value computation on the same memory location, where the value computation uses the value in the memory location

Sequencing rules

Unless otherwise specified evaluations are unsequenced (4.6 [intro.execution]/17). The sequencing rules can be classified into 3 categories (in the standard the rules are given all over the place; search for « sequenced before »):

  • basic rules
  • specific rules
  • rules added in c++17

Sequencing rules : basic rules

The basic rules are:

  1. Given a full expression and the next full expression .
  2. Given an operator @ and operands , val val @.
    (note that nothing is said about the side effects)
  3. In a function call postfix-expression(arg expressions), every argument expression and the postfix-expression designating the call are sequenced before every expression in the function body. Additionally, function calls are indeterminately sequenced if not otherwise sequenced.

References to the standard for the basic rules:

  1. 4.6 [intro.execution]/16
  2. 4.6 [intro.execution]/17
  3. 4.6 [intro.execution]/18 and 8.2.2 [expr.call]/5

Sequencing rules : specific rules

The specific rules are (where @ is a placeholder for one of the appropriate operators):

  1. Postfix ++/-- val @ < se @.
  2. Prefix ++/-- with operand val se @ val @.
  3. Logical && and || with operands and ( @ ) .
  4. Ternary ?: with operands , and ( ? : ) and .
  5. Comma , with operands and (, ) .
  6. (Compound) assignment @ =,+=,-=,*=,/=,%=,^=,|=,&=,<<=,>>=
    with operands and ( @ ) val and val se @ val @.
  7. Braced-init-list { , , …, } we have .
  8. Return statement copy init of the result destruction of temporaries at the end of the full expression destruction of the local variables in the block.
  9. New expression the allocation function is indeterminately sequenced with the expression in the initializer, and the initialization of the allocated object value computation of the new expression.

References to the standard for the specific rules:

  1. 8.2.6 [expr.post.incr]/1
  2. 8.3.2 [expr.pre.incr]/1,2
  3. 8.14 [expr.log.and]/2 and 8.15 [expr.log.or]/2
  4. 8.16 [expr.cond]/1
  5. 8.19 [expr.comma]/1
  6. 8.18 [expr.ass]/1
  7. 11.6.4 [dcl.init.list]/4
  8. 9.6.3 [stmt.return]/3
  9. 8.3.4 [expr.new]/19

Sequencing rules : c++17 rules

The rules added in c++17 are:

  1. Subscript operator [] with operands and ([]) .
  2. Pointer to member operator .* / pointer to member of pointer operator ->* with operands and
    (@) .
  3. Arithmetic shifts <</>> with operands and ( @ ) .
  4. (Compound) assignment (see above rule 6.) @ .
  5. Function call postfix-expression(arg expressions)
    • Postfix-expression arg expressions and default argument expressions.
    • Argument expressions are indeterminately sequenced instead of being unsequenced.
    • For an operator invoked using the operator notation, the operands are sequenced as for built-in operators.
  6. New expression the allocation function is sequenced before the expressions in the initializer.
  7. Parenthesized initializer (, …, ) the are indeterminately sequenced.

References to the standard for the rules added in c++17:

  1. 8.2.1 [expr.sub]/1
  2. 8.5 [expr.mptr.oper]/4
  3. 8.8 [expr.shift]/4
  4. 8.18 [expr.ass]/1
  5. 8.2.2 [expr.call]/5
  6. 8.3.4 [expr.new]/19
  7. 11.6 [dcl.init]/19

Examples

A warning : gcc and clang are supposed to be able to detect undefined behavior caused by the sequencing rules with -Wsequence-point. However as of gcc 7.2 and clang 5 so many false positive are reported as to make this useless. All examples below trigger a warning even when no undefined bahavior is present according to the standard. Of course in practice one should question the wisdom of writing code where a detailed read of the standard is needed to determine its validity.

We new consider some examples to show how the above rules work in action. The examples are roughly ordered in order of increasing complexity. To illustrate them we introduce sequenced-before graphs. A sequenced-before graph is the directed graph obtained from the sequenced-before partial order for some set of value/side effect computations where means that is sequenced-before . Sequenced-before graphs allows us to easily detect if one of the two conditions for undefinedness are present, and if they are not present to decipher the meaning of non-obvious expressions. Without further ado here are the graph legends we will use:

defs

All of the examples below use built-in arithmetic types (say ints).

  1. example1
    in c++14 undefined behavior
    in c++17 well-defined; i is not incremented


  2. example2
    in c++14/17 well-defined; i is incremented


  3. example3
    in c++14 undefined behavior
    in c++17 unspecified behavior; can be either f(i, i+1) or f(i, i). In any case i is guaranteed to have been incremented before the body of the function is entered.


  4. example4
    in c++14 undefined behavior because of all of the red arrows
    in c++17 unspecified behavior; can be either f(i+1, i+2) or f(i+2, i+2). In any case i is guaranteed to have been incremented before the body of the function is entered.


  5. example5
    in c++14 undefined behavior
    in c++17 well-defined; equivalent to (cout << i) << i. Note that postfix ++ has much higher precedence than <<.


  6. example6
    in c++14 undefined behavior because of all of the red arrows
    in c++17 well-defined; leaves i unchanged.


  7. example7
    in c++14/17 undefined behavior


  8. example8
    in c++14/17 well-defined because of the sequencing properties of the logical ||.



  1. example9
    in c++14/17 undefined behavior because accesses to volatile glvalues are side effects.

  2. example10
    in c++14 undefined behavior.
    in c++17 well-defined; sets a[i] to i and then increments i.