I l@ve RuBoard Previous Section Next Section

Solution

graphics/bulb_icon.gif

The Default Constructor

Using the private base class method, our Stack class will look something like this (the code is shown inlined for brevity):



template <class T> 


class Stack : private StackImpl<T>


{


public:


  Stack(size_t size=0)


    : StackImpl<T>(size)


  {


  }


Stack's default constructor simply calls the default constructor of StackImpl, that just sets the stack's state to empty and optionally performs an initial allocation. The only operation here that might throw is the new done in StackImpl's constructor, and that's unimportant when considering Stack's own exception safety. If it does happen, we won't enter the Stack constructor body and there will never have been a Stack object at all, so any initial allocation failures in the base class don't affect Stack. (See Item 8 and More Exceptional C++ Items 17 and 18, for additional comments about exiting constructors via an exception.)

Note that we slightly changed Stack's original constructor interface to allow a starting "hint" at the amount of memory to allocate. We'll make use of this in a minute when we write the Push function.

Guideline

graphics/guideline_icon.gif

Observe the canonical exception-safety rules: Always use the "resource acquisition is initialization" idiom to isolate resource ownership and management.


The Destructor

Here's the first elegance: We don't need to provide a Stack destructor. The default compiler-generated Stack destructor is fine, because it just calls the StackImpl destructor to destroy any objects that were constructed and actually free the memory. Elegant.

The Copy Constructor

Note that the Stack copy constructor does not call the StackImpl copy constructor. (See the previous solution for a discussion of what construct() does.)



Stack(const Stack& other) 


  : StackImpl<T>(other.vused_)


{


  while( vused_ < other.vused_ )


  {


    construct( v_+vused_, other.v_[vused_] );


    ++vused_;


  }


}


Copy construction is now efficient and clean. The worst that can happen here is that a T constructor could fail, in which case the StackImpl destructor will correctly destroy exactly as many objects as were successfully created and then deallocate the raw memory. One big benefit derived from StackImpl is that we could add as many more constructors as we want without putting clean-up code inside each one.

Elegant Copy Assignment

The following is an incredibly elegant and nifty way to write a completely safe copy assignment operator. It's even cooler if you've never seen the technique before.



Stack& operator=(const Stack& other) 


{


  Stack temp(other); // does all the work


  Swap( temp );      // this can't throw


  return *this;


}


Do you get it? Take a minute to think about it before reading on.

This function is the epitome of a very important guideline that we've seen already.

Guideline

graphics/guideline_icon.gif

Observe the canonical exception-safety rules: 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.


It's beautifully elegant, if a little subtle. We just construct a temporary object from other, then call Swap to swap our own guts with temp's, and, finally, when temp goes out of scope and destroys itself, it automatically cleans up our old guts in the process, leaving us with the new state.

Note that when operator=() is made exception-safe like this, a side effect is that it also automatically handles self-assignment (for example, Stack s; s = s;) correctly without further work. (Because self-assignment is exceedingly rare, I omitted the traditional if( this != &other ) test, which has its own subtleties. See Item 38 for all the gory details.)

Note that because all the real work is done while constructing temp, any exceptions that might be thrown (either by memory allocation or T copy construction) can't affect the state of our object. Also, there won't be any memory leaks or other problems from the temp object, because the Stack copy constructor is already strongly exception-safe. Once all the work is done, we simply swap our object's internal representation with temp's, which cannot throw (because Swap has a throw() exception specification, and because it does nothing but copy builtins), and we're done.

Note especially how much more elegant this is than the exception-safe copy assignment we implemented in Item 9. This version also requires much less care to ensure that it's been made properly exception-safe.

If you're one of those folks who like terse code, you can write the operator=() canonical form more compactly by using pass-by-value to create the temporary:



Stack& operator=(Stack temp) 


{


  Swap( temp );


  return *this;


}


Stack<T>::Count()

Yes, Count() is still the easiest member function to write.



size_t Count() const 


{


  return vused_;


}


Stack<T>::Push()

Push() needs a little more attention. Study it for a moment before reading on.



void Push( const T& t ) 


{


  if( vused_ == vsize_ )  // grow if necessary


  {


    Stack temp( vsize_*2+1 );


    while( temp.Count() < vused_ )


    {


      temp.Push( v_[temp.Count()] );


    }


    temp.Push( t );


    Swap( temp );


  }


  else


  {


    construct( v_+vused_, t );


    ++vused_;


  }


}


First, consider the simple else case: If we already have room for the new object, we attempt to construct it. If the construction succeeds, we update our vused_ count. This is safe and straightforward.

Otherwise, like last time, if we don't have enough room for the new element, we trigger a reallocation. In this case, we simply construct a temporary Stack object, push the new element onto that, and finally swap out our original guts to it to ensure they're disposed of in a tidy fashion.

But is this exception-safe? Yes. Consider:

  • If the construction of temp fails, our state is unchanged and no resources have been leaked, so that's fine.

  • If any part of the loading of temp's contents (including the new object's copy construction) fails by throwing an exception, temp is properly cleaned up when its destructor is called as temp goes out of scope.

  • In no case do we alter our state until all the work has already been completed successfully.

Note that this provides the strong commit-or-rollback guarantee, because the Swap() is performed only if the entire reallocate-and-push operation succeeds. Any references returned from Top(), or iterators if we later chose to provide them, would never be invalidated (by a possible internal grow operation) if the insertion is not completely successful.

Stack<T>::Top()

Top() hasn't changed at all.



T& Top() 


{


  if( vused_ == 0 )


  {


    throw "empty stack";


  }


  return v_[vused_-1];


}


Stack<T>::Pop()

Neither has Pop(), save the new call to destroy().



void Pop() 


{


  if( vused_ == 0 )


    {


      throw "pop from empty stack";


    }


    else


    {


      --vused_;


      destroy( v_+vused_ );


    }


  }


};


In summary, Push() has been simplified, but the biggest benefit of encapsulating the resource ownership in a separate class was seen in Stack's constructor and destructor. Thanks to StackImpl, we can go on to write as many more constructors as we like, without having to worry about clean-up code, whereas last time each constructor would have had to know about the clean-up itself.

You may also have noticed that even the lone try/catch we had to include in the first version of this class has now been eliminated梩hat is, we've written a fully exception-safe and exception-neutral generic container without writing a single try! (Who says writing exception-safe code is trying?)

    I l@ve RuBoard Previous Section Next Section