How is an expression like (a + b) * (c +
d)
evaluated? In a serial implementation, you might
expect that a + b
is evaluated first,
then c + d
, with the product of those
two results computed last. However, the nature of the
expression only defines a partial ordering, specifically that
the product must be computed after the sums, but the sums
could be computed in either order. Indeed, in a parallel
implementation, the two sums could be computed
simultaneously.
For expressions without side-effects, this unspecified ordering is not
important. However, it is highly significant in an expression
such as (a + x++) * (c * x++)
, as the
side-effects could be performed in any order, and alter the
result on the other side of the expression. And what about an
expression such as (a + x++) * (c *
(*p)++)
, where p
might be
pointing to
x
, or even a
or
c
?
In these cases, no guarantees are made about the behaviour of the expression; it is undefined, and the compiler doesn't even have to warn you about it.
Such problematic expressions are characterized by having either at least two writes to the same object, or at least one write and one read of the same object. However, an expression is safe if, between every pair of writes to the same object, and between every pair of one read and one write on the same object, there is a sequence point.
Sequence points are defined to occur on specific operators between their operands.
There is a sequence point, for example, between the operands
of &&
. A read or write of a single
object on its left-hand side is guaranteed to happen before a
read or write of the same object on the right-hand side.
However, there can still be a problem if there are two writes
on the left, or two on the right, unless another sequence
point can be found between them.
Sequence points are found on these operators:
- between the operands of
&&
, - between the operands of
||
, - between the operands of
,
, - between the first operand of
?:
and the other two, and - between the function designator and its arguments in a function call.
There is also effectively a sequence point between two statements, and between top-level initializers.
The Standard Library also guarantees sequence points: