I l@ve RuBoard Previous Section Next Section

Solution

graphics/bulb_icon.gif

We won't spend much time analyzing why the following functions are fully exception-safe (work properly in the presence of exceptions) and exception-neutral (propagate all exceptions to the caller), because the reasons are pretty much the same as those we discussed in detail in the first half of this miniseries. But do take a few minutes now to analyze these solutions, and note the commentary.

Constructor

The constructor is fairly straightforward. We'll use operator new() to allocate the buffer as raw memory. (Note that if we used a new-expression like new T[size], then the buffer would be initialized to default-constructed T objects, which was explicitly disallowed in the problem statement.)



template <class T> 


StackImpl<T>::StackImpl( size_t size )


  : v_( static_cast<T*>


          ( size == 0


            ? 0


            : operator new(sizeof(T)*size) ) ),


    vsize_(size),


    vused_(0)


{


}


Destructor

The destructor is the easiest of the three functions to implement. Again, remember what we learned about operator delete() earlier in this miniseries. (See "Some Standard Helper Functions" for full details about functions such as destroy() and swap() that appear in the next few pieces of code.)



template <class T> 


StackImpl<T>::~StackImpl()


{


    destroy( v_, v_+vused_ ); // this can't throw


    operator delete( v_ );


}


We'll see what destroy() is in a moment.

Some Standard Helper Functions

The Stack and StackImpl presented in this solution use three helper functions, one of which (swap()) also appears in the standard library: construct(), destroy(), and swap(). In simplified form, here's what these functions look like:



// construct() constructs a new object in 


// a given location using an initial value


//


template <class T1, class T2>


void construct( T1* p, const T2& value )


{


  new (p) T1(value);


}


The above form of new is called "placement new," and instead of allocating memory for the new object, it just puts it into the memory pointed at by p. Any object new'd in this way should generally be destroyed by calling its destructor explicitly (as in the following two functions), rather than by using delete.



// destroy() destroys an object or a range 


// of objects


//


template <class T>


void destroy( T* p )


{


  p->~T();


}


template <class FwdIter>


void destroy( FwdIter first, FwdIter last )


{


  while( first != last )


  {


    destroy( &*first );


    ++first;


  }


}


// swap() just exchanges two values


//


template <class T>


void swap( T& a, T& b )


{


  T temp(a); a = b; b = temp;


}


Of these, destroy(first,last) is the most interesting. We'll return to it a little later in the main miniseries; it illustrates more than one might think!

Swap

Finally, a simple but very important function. Believe it or not, this is the function that is instrumental in making the complete Stack class so elegant, especially its operator=(), as we'll see soon.



template <class T> 


void StackImpl<T>::Swap(StackImpl& other) throw()


{


    swap( v_,     other.v_ );


    swap( vsize_, other.vsize_ );


    swap( vused_, other.vused_ );


}


To picture how Swap() works, say that you have two StackImpl<T> objects a and b, as shown in Figure 1.

Figure 1. Two StackImpl<T> objects a and b

graphics/02fig01.gif

Then executing a.Swap(b) changes the state to that shown in Figure 2.

Figure 2. The same two StackImpl<T> objects, after a.Swap(b)

graphics/02fig02.gif

Note that Swap() supports the strongest exception guarantee of all梟amely, the nothrow guarantee; Swap() is guaranteed not to throw an exception under any circumstances. It turns out that this feature of Swap() is essential, a linchpin in the chain of reasoning about Stack's own exception safety.

Why does StackImpl exist? Well, there's nothing magical going on here: StackImpl is responsible for simple raw memory management and final cleanup, so any class that uses it won't have to worry about those details.

Guideline

graphics/guideline_icon.gif

Prefer cohesion. Always endeavor to give each piece of code梕ach module, each class, each function梐 single, well-defined responsibility.


So what access specifier would you write in place of the comment "/*????*/"? Hint: The name StackImpl itself hints at some kind of "implemented-in-terms-of " relationship, and there are two main ways to write that kind of relationship in C++.

Technique 1: Private Base Class.The missing /*????*/ access specifier must be either protected or public. (If it were private, no one could use the class.) First, consider what happens if we make it protected.

Using protected means that StackImpl is intended to be used as a private base class. So Stack will be "implemented in terms of " StackImpl, which is what private inheritance means, and we have a clear division of responsibilities. The StackImpl base class will take care of managing the memory buffer and destroying all remaining T objects during Stack destruction, while the Stack derived class will take care of constructing all T objects within the raw memory. The raw memory management takes place pretty much entirely outside Stack itself, because, for example, the initial allocation must fully succeed before any Stack constructor body can be entered. Item 13 begins the final phase of this miniseries, in which we'll concentrate on implementing this version.

Technique 2: Private Member. Next, consider what happens if StackImpl's missing /*????*/ access specifier is public.

Using public hints that StackImpl is intended to be used as a struct by some external client, because its data members are public. So again, Stack will be "implemented in terms of " StackImpl, only this time using a HAS-A containment relationship instead of private inheritance. We still have the same clear division of responsibilities. The StackImpl object will take care of managing the memory buffer and destroying all T objects remaining during Stack destruction, and the containing Stack will take care of constructing T objects within the raw memory. Because data members are initialized before a class's constructor body is entered, the raw memory management still takes place pretty much entirely outside Stack, because, for example, the initial allocation must fully succeed before any Stack constructor body can be entered.

As we'll see when we look at the code, this second technique is only slightly different from the first.

    I l@ve RuBoard Previous Section Next Section