I l@ve RuBoard |
SolutionThis brings us to a key topic, namely the innocent looking delete[] p;. What does it really do? And how safe is it? Destructors That Throw and Why They're EvilFirst, recall our standard destroy helper function (see the accompanying box): template <class FwdIter> void destroy( FwdIter first, FwdIter last ) { while( first != last ) { destroy( &*first ); // calls "*first"'s destructor ++first; } } This was safe in our example above, because we required that T destructors never throw. But what if a contained object's destructor were allowed to throw? Consider what happens if destroy is passed a range of five objects. If the first destructor throws, then as it is written now, destroy will exit and the other four objects will never be destroyed. This is obviously not a good thing. "Ah," you might interrupt, "but can't we clearly get around that by writing destroy to work properly in the face of T's whose destructors are allowed to throw?" Well, that's not as clear as one might think. For example, you'd probably start writing something like this: template <class FwdIter> void destroy( FwdIter first, FwdIter last ) { while( first != last ) { try { destroy( &*first ); } catch(...) { /* what goes here? */ } ++first; } } The tricky part is the "what goes here?" There are really only three choices: the catch body rethrows the exception, it converts the exception by throwing something else, or it throws nothing and continues the loop.
However, it obviously fails to meet the neutrality requirement that T exceptions be allowed to pass through because exceptions are absorbed and ignored (as far as the caller is concerned, even if the catch body does attempt to do some sort of logging). I've seen people suggest that the function should catch the exception and "save" it while continuing to destroy everything else, then rethrow it at the end. That too isn't a solution—for example, it can't correctly deal with multiple exceptions should multiple T destructors throw (even if you save them all until the end, you can end by throwing only one of them and the others are silently absorbed). You might be thinking of other alternatives, but trust me, they all boil down to writing code like this somewhere, because you have a set of objects and they all need to be destroyed. Someone, somewhere, is going to end up writing exception-unsafe code (at best) if T destructors are ever allowed to throw. Which brings us to the innocent-looking new[] and delete[]. The issue with both of these is that they have fundamentally the same problem we just described for destroy. For example, consider the following code: T* p = new T[10]; delete[] p; Looks like normal, harmless C++, doesn't it? But have you ever wondered what new[] and delete[] do if a T destructor throws? Even if you have wondered, you can't know the answer for the simple reason that there is none. The standard says you get undefined behavior if a T destructor throws anywhere in this code, which means that any code that allocates or deallocates an array of objects whose destructors could throw can result in undefined behavior. This may raise some eyebrows, so let's see why this is so. First, consider what happens if the constructions all succeed and, then, during the delete[] operation, the fifth T destructor throws. Then delete[], has the same catch-22 problem[12] outlined above for destroy. It can't allow the exception to propagate because then the remaining T objects would be irretrievably undestroyable, but it also can't translate or absorb the exception because then it wouldn't be exception-neutral.
Second, consider what happens if the fifth constructor throws. Then the fourth object's destructor is invoked, then the third's, and so on until all the T objects that were successfully constructed have again been destroyed, and the memory is safely deallocated. But what if things don't go so smoothly? In particular, what if, after the fifth constructor throws, the fourth object's destructor throws? And, if that's ignored, the third's? You can see where this is going. If destructors may throw, then neither new[] nor delete[] can be made exception-safe and exception-neutral. The bottom line is simply this: Don't ever write destructors that can allow an exception to escape.[13] If you do write a class with such a destructor, you will not be able to safely even new[] or delete[] an array of those objects. All destructors should always be implemented as though they had an exception specification of throw()—that is, no exceptions must ever be allowed to propagate.
Guideline
Granted, some may feel that this state of affairs is a little unfortunate, because one of the original reasons for having exceptions was to allow both constructors and destructors to report failures (because they have no return values). This isn't quite true, because the intent was mainly for constructor failures (after all, destructors are supposed to destroy, so the scope for failure is definitely less). The good news is that exceptions are still perfectly useful for reporting construction failures, including array and array-new[] construction failures, because there they can work predictably, even if a construction does throw. Safe ExceptionsThe advice "be aware, drive with care" certainly applies to writing exception-safe code for containers and other objects. To do it successfully, you do have to meet a sometimes significant extra duty of care. But don't get unduly frightened by exceptions. Apply the guidelines outlined above—that is, isolate your resource management, use the "update a temporary and swap" idiom, and never write classes whose destructors can allow exceptions to escape—and you'll be well on your way to safe and happy production code that is both exception-safe and exception-neutral. The advantages can be both concrete and well worth the trouble for your library and your library's users. For your convenience (and, hopefully, your future review), here is the "exception safety canonical form" summarized in one place. Guideline
|
I l@ve RuBoard |