At its most fundamental level, the execution of a C++ program is the successive evaluation of expressions, under the control of statements (Chapter 4), in which some expressions can produce side effects. Any expression might have one or more of the following side effects:
Accessing a volatile object
Modifying an object
Calling a function in the standard library
Calling any other function that has side effects
During the execution of a program, there are well-defined points
in time called sequence points, at which the side effects
have all been completed for expressions that have been evaluated, and
no side effects have been started for any unevaluated expression.
Between sequence points, the compiler is free to reorder expressions
in any way that preserves the original semantics. The same term also
refers to the positions in the source code that produce sequence
points when the code executes. You can usually ignore the details of
sequence points, but when you are using global or volatile
objects, it is important that you know exactly when it
is safe to access those objects. That time is after a sequence point.
Also, any expression that modifies a scalar object more than once
between sequence points, or that examines a scalar object's value
after modifying it, yields undefined behavior. This rule often bites
the unwary programmer who uses the increment and decrement operators.
For example:
int i = 0; i = ++i - ++i; // Error: undefined behavior printf("%d,%d", ++i, ++i); // Error: undefined behavior i = 3, ++i, i++; // OK: i == 5
There are sequence points in the following positions:
At the end of every expression that is not a subexpression.
Such an expression might be used in an expression statement, in an
initializer, as a condition in an if
statement, etc.
After evaluating all function arguments but before calling the function.
When a function returns: after copying the return value from the function call (if any), but before evaluating any other expressions outside the function.
After evaluating the first expression (expr1
) in each of the following
expressions, provided they use the built-in operators and not
overloaded operators:
expr1 &&
expr2
expr1 || expr2
expr1 ? expr2 :
expr3
expr1 , expr2
In general, the order in which operands are evaluated is unspecified, so
you should never write code that depends on a particular order. For
example, in the expression f( )
/
g(
)
, f( )
might be called
first, or g( )
might be called
first. The difference can be significant when the functions have side
effects. Example 3-2 shows a
contrived situation in which a program prints 2
if g( )
is called first, or 1
if f( )
is called first.
Example 3-2. Demonstrating order of evaluation
#include <iostream> #include <ostream> int x = 1; int f( ) { x = 2; return x; } int g( ) { return x; } int main( ) { std::cout << f( ) / g( ) << '\n'; }
A simpler example follows. The increment of i
can happen before or after the assignment,
so i
might be 2
or 3
.
int i = 1; i = i++ + 1; // Value of i is unspecified
In a function call, all arguments are evaluated before the function is called. As you might expect, the order in which the arguments are evaluated is unspecified.
The logical operators (&&
and ||
) perform short-circuit
evaluation . The left operand is evaluated, and if the expression
result can be known at that point, the right operand is
not evaluated:
if (false && f( )) ... // f( ) is never called. if (true || f( )) ... // f( ) is never called.
If the logical operator is overloaded, however, it cannot
perform short-circuit evaluation. Like any other function, all the
arguments are evaluated before the function is called. For this
reason, you should avoid overloading the &&
and ||
operators.