Team LiB   Previous Section   Next Section

3.4 Expression Evaluation

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

3.4.1 Sequence Points

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

3.4.2 Order of Evaluation

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.

3.4.3 Short-Circuit Evaluation

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.

    Team LiB   Previous Section   Next Section