I l@ve RuBoard Previous Section Next Section

Solution

graphics/bulb_icon.gif

Right away, we can see that Stack is going to have to manage dynamic memory resources. Clearly, one key is going to be avoiding leaks, even in the presence of exceptions thrown by T operations and standard memory allocations. For now, we'll manage these memory resources within each Stack member function. Later on in this miniseries, we'll improve on this by using a private base class to encapsulate resource ownership.

Default Construction

First, consider one possible default constructor:



// Is this safe? 





template<class T>


Stack<T>::Stack()


: v_(0),


  vsize_(10),


  vused_(0)         // nothing used yet


{


  v_ = new T[vsize_]; // initial allocation


}


Is this constructor exception-safe and exception-neutral? To find out, consider what might throw. In short, the answer is: any function. So the first step is to analyze this code and determine which functions will actually be called, including both free functions and constructors, destructors, operators, and other member functions.

This Stack constructor first sets vsize_ to 10, then attempts to allocate some initial memory using new T[vsize_]. The latter first tries to call operator new[]() (either the default operator new[]() or one provided by T) to allocate the memory, then tries to call T::T a total of vsize_ times. There are two operations that might fail. First, the memory allocation itself, in which case operator new[]() will throw a bad_alloc exception. Second, T's default constructor, which might throw anything at all, in which case any objects that were constructed are destroyed and the allocated memory is automatically guaranteed to be deallocated via operator delete[]().

Hence the above function is fully exception-safe and exception-neutral, and we can move on to the next…what? Why is the function fully robust, you ask? All right, let's examine it in a little more detail.

  1. We're exception-neutral. We don't catch anything, so if the new throws, then the exception is correctly propagated up to our caller as required.

    Guideline

    graphics/guideline_icon.gif

    If a function isn't going to handle (or translate or deliberately absorb) an exception, it should allow the exception to propagate up to a caller who can handle it.


  2. We don't leak. If the operator new[]() allocation call exited by throwing a bad_alloc exception, then no memory was allocated to begin with, so there can't be a leak. If one of the T constructors threw, then any T objects that were fully constructed were properly destroyed and, finally, operator delete[]() was automatically called to release the memory. That makes us leakproof, as advertised.

    I'm ignoring for now the possibility that one of the T destructor calls might throw during the cleanup, which would call terminate() and simply kill the program altogether and leave events well out of your control anyway. See the point in Item 16 in which we cover information on "destructors that throw and why they're evil."

  3. We're in a consistent state whether or not any part of the new throws. Now one might think that if the new throws, then vsize_ has already been set to 10 when, in fact, nothing was successfully allocated. Isn't that inconsistent? Not really, because it's irrelevant. Remember, if the new throws, we propagate the exception out of our own constructor, right? And, by definition, "exiting a constructor by means of an exception" means our Stack proto-object never actually got to become a completely constructed object at all. Its lifetime never started, so its state is meaningless because the object never existed. It doesn't matter what the memory that briefly held vsize_ was set to, any more than it matters what the memory was set to after we leave an object's destructor. All that's left is raw memory, smoke, and ashes.

Guideline

graphics/guideline_icon.gif

Always structure your code so that resources are correctly freed and data is in a consistent state even in the presence of exceptions.


All right, I'll admit it: I put the new in the constructor body purely to open the door for that third discussion. What I'd actually prefer to write is:



template<class T> 


Stack<T>::Stack()


  : v_(new T[10]),  // default allocation


    vsize_(10),


    vused_(0)       // nothing used yet


{


}


Both versions are practically equivalent. I prefer the latter because it follows the usual good practice of initializing members in initializer lists whenever possible.

Destruction

The destructor looks a lot easier, once we make a (greatly) simplifying assumption.



template<class T> 


Stack<T>::~Stack()


{


  delete[] v_;      // this can't throw


}


Why can't the delete[] call throw? Recall that this invokes T::~T for each object in the array, then calls operator delete[]() to deallocate the memory. We know that the deallocation by operator delete[]() may never throw, because the standard requires that its signature is always one of the following:[2]

[2] As Scott Meyers pointed out in private communication, strictly speaking this doesn't prevent someone from providing an overloaded operator delete[] that does throw, but any such overload would violate this clear intent and should be considered defective.



void operator delete[]( void* ) throw(); 


void operator delete[]( void*, size_t ) throw();


Hence, the only thing that could possibly throw is one of the T::~T calls, and we're arbitrarily going to have Stack require that T::~T may not throw. Why? To make a long story short, we just can't implement the Stack destructor with complete exception safety if T::~T can throw, that's why. However, requiring that T::~T may not throw isn't particularly onerous, because there are plenty of other reasons why destructors should never be allowed to throw at all.[3] Any class whose destructor can throw is likely to cause you all sorts of other problems sooner or later, and you can't even reliably new[] or delete[] an array of them. More on that as we continue in this miniseries.

[3] Frankly, you won't go far wrong if you habitually write throw() after the declaration of every destructor you ever write. Even if exception specifications cause expensive checks under your current compiler, at least write all your destructors as though they were specified as throw()梩hat is, never allow exceptions to leave destructors.

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()". More on this as we go on; this is an important theme.


    I l@ve RuBoard Previous Section Next Section