I l@ve RuBoard previous section next section

Solution

graphics/bulb_icon.gif

This 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 Evil

First, 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.

  1. If the catch body rethrows the exception, then the destroy function nicely meets the requirement of being exception-neutral, because it does indeed allow any T exceptions to propagate out normally. However, it still doesn't meet the safety requirement that no resources be leaked if exceptions occur. Because destroy has no way of signaling how many objects were not successfully destroyed, those objects can never be properly destroyed, so any resources associated with them will be unavoidably leaked. Definitely not good.

  2. If the catch body converts the exception by throwing something else, we've clearly failed to meet both the neutrality and the safety requirements. Enough said.

  3. If the catch body does not throw or rethrow anything, then the destroy function nicely meets the safety requirement that no resources be leaked if an exception is thrown.[11]

    [11] True, if a T destructor could throw in a way that its resources might not be completely released, then there could still be a leak. However, this isn't destroy's problem...this just means that T itself is not exception-safe. But destroy is still properly leak-free in that it doesn't fail to release any resources that it should (namely the T objects themselves).

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.

[12] Pun intended.

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.

[13] As of the London meeting in July of 1997, the draft makes the blanket statement: "No destructor operation defined in the C++ Standard Library will throw an exception." Not only do all the standard classes have this property, but in particular it is not permitted to instantiate a standard container with a type whose destructor does throw. The rest of the guarantees I'm going to outline were fleshed out at the following meeting (Morristown, N.J., November 1997, which was the meeting at which the completed standard was voted out).

Guideline

graphics/guideline_icon.gif

Observe the canonical exception safety rules: Never allow an exception to escape from a destructor or from an overloaded operator delete() or operator delete[](); write every destructor and deallocation function as though it had an exception specification of "throw()."


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 Exceptions

The 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

graphics/guideline_icon.gif

Observe the canonical exception-safety rules: (1) Never allow an exception to escape from a destructor or from an overloaded operator delete() or operator delete[](); write every destructor and deallocation function as though it had an exception specification of "throw()." (2) Always use the "resource acquisition is initialization" idiom to isolate resource ownership and management. (3) In each function, take all the code that might emit an exception and do all that work safely off to the side. Only then, when you know that the real work has succeeded, should you modify the program state (and clean up) using only nonthrowing operations.


I l@ve RuBoard previous section next section