[ Team LiB ] |
![]() ![]() |
Gotcha #87: Problems with Increment and DecrementEven the best C programmers tend to use prefix and postfix increment and decrement interchangeably when either will do: int j; for( j = 0; j < max; j++ ) /* OK, in C. */ However, this is now considered démodé in C++, and prefix increment and decrement are always to be preferred over the postfix versions of the operators when either will do. The reason has to do with operator overloading. Increment and decrement operators are often overloaded to support operations on iterators or smart pointers. These may be member or non-member operators but are commonly implemented as members for class types: class Iter { public: Iter &operator ++(); // prefix Iter operator ++(int); // postfix Iter &operator --(); // prefix Iter operator --(int); // postfix // . . . }; The prefix forms should return a modifiable lvalue, to mimic the behavior of the predefined operators. Effectively, this is a requirement that the operator return a reference to its object: Iter &Iter::operator ++() { // increment *this . . . return *this; } // . . . int j = 0; ++++j; // OK, but j+=2 might be better Iter i; ++++++++i; // OK, but odd The postfix forms are distinguished from the prefix forms by the addition of an unused integer argument. The compiler simply passes a zero actual argument to distinguish postfix from prefix: Iter i; ++i; // same as i.operator ++(); i++; // same as i.operator ++(0); i.operator ++(1024); // legal, but strange Typically, the implementations of the postfix operators ignore the integer argument. To mimic the behavior of the predefined postfix increment and decrement operators, an overloaded version must return a copy of its object containing the object's value before the increment operation. The postfix operator is commonly implemented in terms of the corresponding prefix operator: Iter Iter::operator ++(int) { Iter temp( *this ); ++*this; return temp; } Effectively, this is a requirement that the postfix operator return by value. Even with the possible application of common program transformations like the named return value optimization (see Gotcha #58), use of a postfix increment or decrement operator is likely to be slower than that of the corresponding prefix version if the argument is of class type. Consider a common use within the standard template library:
vector<T> v;
// . . .
vector<T>::iterator end( v.end() );
for( vector<T>::iterator vi( v.begin() ); vi != end;
vi++ ) { // gauche!
// . . .
The implementation of the vector's iterator could be a simple pointer, in which case the runtime effect of postfix increment is nonexistent, or the iterator could be of class type, in which case the effect could be significant. For this reason, it's considered better form in C++ to always prefer prefix forms to postfix forms. Many implementations of generic algorithms take this advice even further (perhaps too far), and avoid the use of postfix increment and decrement at any cost: template <typename In, typename Out> Out myCopy( In b, In e, Out r ) { while( b != e ) { // rather than *r++ = *b++ *r = *b; ++r; ++b; } return r; } Note that the predefined postfix increment and decrement operators return an rvalue; that is, the result of the operation has no address and can't be used with operators that require an lvalue (see Gotcha #6): int a = 12; ++a = 10; // OK ++++a; // OK a++ = 10; // error! a++++; // error! Unfortunately, our earlier implementation of postfix ++ returns an anonymous temporary class object generated by the compiler. According to the standard it's not an lvalue, but we can still call the member functions of such an object, which means it can be incremented and assigned. But the incremented and assigned temporary is destroyed at the end of the expression!
Iter i;
Iter j;
++i = j; // OK
i++ = j; // legal, but should be an error!
The value of i is unaffected by the assignment from j, since the assignment is to an anonymous temporary that (presumably) contains the value of i before it was post-incremented. A safer implementation of a user-defined postfix increment or decrement would return a constant: class Iter { public: Iter &operator ++(); // prefix const Iter operator ++(int); // postfix Iter &operator --(); // prefix const Iter operator --(int); // postfix // . . . }; // . . . i++ = j; // error! i++++; // error! This will prevent most accidental misuses of the return value of postfix increment and decrement but will not protect against willful abuse. The return value is not modifiable, but it still has an address:
const Iter *ip = &i++;
This clever programmer has managed to take the address not of i but of a compiler-generated temporary that's destroyed immediately after the pointer is initialized. This is an act of programming fraud with malice aforethought and will be punished appropriately (see Gotcha #11). We mentioned above that most user-defined increment and decrement operators are implemented as member functions. This is not the case, of course, for increment and decrement for enum types, since they can have no member functions: enum Sin { pride, covetousness, lust, anger, gluttony, envy, sloth, future_use, num_sins }; inline Sin &operator ++( Sin &s ) { return s = static_cast<Sin>(s+1); } inline const Sin operator ++( Sin &s, int ) { Sin ret( s ); s = ++s; return ret; } Note the absence of range checking in these functions. A programmer who has chosen to represent a concept as an enum rather than as a more sophisticated class type is probably doing so for reasons of efficiency. Any attempt to perform range checking on such a type is likely to defeat the original intent of using the type. It will also probably result in a lot of unnecessary double-checking of end conditions: for( Sin s = pride; s != num_sins; ++s ) // . . . ![]() |
[ Team LiB ] |
![]() ![]() |