Saturday, October 17, 2009

Episode Ten: The Natural Order of Things

While each statement is evaluated after the previous one, the evaluation of individual sub-expressions is left unspecified in C++. This is supposed to allow producing optimal code for the target platform, as opposed to intuitive left-to-right evaluation everywhere. Code relying on the implementation defined evaluation order is thus non-portable. This means that `v[i] = i++`, `v[i] + i++` and even `f(v[i], i++)` are all examples of non-portable code. Generally speaking, when a variable is read twice and also written in the same expression the result is undefined.

The standard defines a few exceptions where evaluation order is specified:



· Operands of the binary logical operators (&& and ||) are evaluated left-to-right, and short-circuited.

· Operands of the comma operator (,) are evaluated left-to-right.

· The first operand of the conditional operator (?:) is completely evaluated before continuing. Only one of the second and third expressions is evaluated.

·  Initialization of class bases and members is evaluated in a strict order, so to guarantee destruction in the reverse order. Direct base classes are initialized first, in the order they appear in the base-specifier-list. Then, non-static members are initialized in the order they were declared in the class definition.

When operators are overloaded, the semantics became those of function-call expressions. So the evaluation order for overloaded binary logical operators and the overloaded comma operator is again unspecified. This means special care must be taken when working with templates, where the concrete type is unknown. The evaluation order for expressions including those operators is dependant on whether the provided type overloads such operators; and for binary logical operators it also means that short-circuited evaluation is not guaranteed. This is why overloading them is sometimes considered rude.

Assuming a type overloads operators with their usual meaning, some protection can be achieved by wrapping them in a type for which they are not:
template< typename T >
struct protected_
{
    protected_( T value ) : _value( value ) {}
    T value() const { return _value; }
    operator unspecified-bool-type() const { return _value; }

    T _value;
};

template< typename T >
protected_< T& > protect( T& v ){ return v; }
Considering an evil_int type that behaves like an integer and overloads every operator with their usual meaning,
evil_int value1 = 0, value2 = 0;
std::cout
    << ( ++value1, value1++ ) << ", "
    << ( value2 && ++value2 ) << std::endl;
results in "0, true" (for a particular C++ implementation); while
evil_int value1 = 0, value2 = 0;
std::cout
    << ( protect( ++value1 ), protect( value1++ ) ).value()
    << ", "
    << ( protect( value2 ) && protect( ++value2 ) )
    << std::endl;
results in "1, false" as expected for an integral-like type.

No comments:

Post a Comment