I l@ve RuBoard | ![]() ![]() |
Solution![]() 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 ConstructionFirst, 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.
Guideline
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. DestructionThe 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]
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.
Guideline
|
I l@ve RuBoard | ![]() ![]() |