More Exceptional C++: 40 New Engineering Puzzles, Programming Problems, and Solutions
| I l @ ve RuBoard |
| 1. Describe what the following code does: // Example 9-1 // f( a++ ); Be as complete as you can about all possibilities. A comprehensive list would be daunting, but here are the main possibilities. First, f could be any of the following: 1. A macro.
In this case, the statement could mean just about anything, and a++ could be evaluated many times or not at all. For example: #define f(x) x // once #define f(x) (x,x,x,x,x,x,x,x,x) // 9 times #define f(x) // not at all
Guideline
2. A function.
In this case, first a++ is evaluated and then the result is passed to the function as its parameter. Normally, postincrement returns the previous value of a in the form of a temporary object, so f() could take its parameter either by value or by reference to const , but not by reference to non- const , because a reference to non- const cannot be bound to a temporary object. 3. An object.
In this case, f would be a function object, that is, an object for which operator()() is defined. Again, if postincrement returns the previous value of a (as postincrement always should) then f 's operator()() could take its parameter either by value or by reference to const . 4. A type name .
In this case, the statement first evaluates a++ and uses the result of that expression to initialize a temporary object of type f . * * * * * Next, a could be: 1. A macro.
In this case, again, a could mean just about anything. 2. An object (possibly of a built-in type).
In this case, it must be a type for which a suitable operator++(int) postincrement operator is defined. Normally, postincrement should be implemented in terms of preincrement and should return the previous value of a : // Canonical form of postincrement: T T::operator++(int) { T old( *this ); // remember our original value ++*this; // always implement postincrement // in terms of preincrement return old; // return our original value } When you overload an operator, of course, you do have the option of changing its normal semantics to do "something unusual." For example, the following is likely to break the Example 9-1 code for most kinds of f , assuming a is of type A : void A::operator++(int) // doesn't return anything Don't do that. Instead, follow this sound advice:
Guideline
3. A value, such as an address.
For example, a could be a pointer. Some Effects of Side Effects
For the remaining questions, I will make the simplifying assumptions that f() is not a macro, and that a is an object with natural postincrement semantics. 2. What is the difference, if any, between the following two code fragments ? // Example 9-2(a) // f( a++ ); Example 9-2(a) performs these steps:
Example 9-2(a) ensures that the postincrement is performed, and therefore a gets its new value, before f() executes. As already noted, f() could still be a function, a function object, or a type name that leads to a constructor call. Some coding standards state that operations like ++ should always appear on separate lines, on the grounds that it can be dangerous to perform multiple operations like ++ in the same statement because of sequence points (more about this in Items 20 and 21). Instead, such coding standards would recommend Example 9-2(b): // Example 9-2(b) // f( a ); a++; This example performs the following steps:
In both cases, f() gets the old value of a . "So what's the big difference?" you may ask. Well, Example 9-2(b) will not always have the same effect as Example 9-2(a), because Example 9-2(b) ensures that the postincrement is performed, and therefore a gets its new value, after f() executes. This has two major consequences. First, in the case in which f() emits an exception, Example 9-2(a) ensures that a++ and all its side effects have already been completed successfully; whereas Example 9-2(b) ensures that a++ has not been performed, and none of its side effects has occurred. Second, even in non-exceptional cases, if f() and a . operator++(int) have visible side effects, the order in which they are executed can matter. More specifically , consider what happens if f() has a side effect that affects the state of a itself. That's neither farfetched nor unlikely , and it can happen even if f() doesn't and can't directly change a , as we'll now illustrate with an example. Scissors, Traffic, and Iterators
3. In Question #2, make the simplifying assumption that f() is a function that takes its argument by value, and that a is an object of class type that has an operator++(int) with natural semantics. Now what is the difference, if any, between Example 9-2(a) and Example 9-2(b)? The difference is that, for perfectly normal C++ code, Example 9-2(a) can be legal when Example 9-2(b) is not. This is because in Example 9-2(a) there is a period of time during which objects exist simultaneously with the old and new values of a . In Example 9-2(b), there is no such overlap. Consider what happens when we replace f() with list::erase() , and a with a list::iterator . Now the first form is valid: // Example 9-3(a) // // l is a list<int> // i is a valid non-end iterator into l // l.erase( i++ ); // OK, incrementing a valid iterator But the second form is not: // Example 9-3(b) // // l is a list<int> // i is a valid non-end iterator into l // l.erase( i ); i++; // error, i is not a valid iterator The reason that Example 9-3(b) is incorrect is that the call to l . erase( i ) invalidates i , so you can no longer call operator++() on i afterward. Warning: Some programmers do write code like Example 9-3(b), perhaps because of coding guidelines that have a blanket policy of discouraging operations like ++ in function call statements. Programmers who write code like Example 9-3(b) may even be routinely getting away with it (and not realizing the danger) just because it happens to work on the current version of their compiler and library. But let them be warned : Code like Example 9-3(b) is not portable; it is not sanctioned by the standard; and it's likely to turn and bite them when they port to another compiler platform, or even just upgrade the one they're working on today. When it does bite, it will bite hard, because "using-an-invalid-iterator" bugs can be very difficult to findunless you have the joy of working with a good checked library implementation during debugging, but then you'd already have been warned about such errors. Some mothers (who are also software engineers ) give the following three pieces of good advice, and we should always strive to follow them for our own good:
Having said that, with the exception of examples like the above, we'll see in Items 20 and 21 why it's still a good idea in general to avoid writing operations like ++ in function calls. |
| I l @ ve RuBoard |