I l@ve RuBoard | ![]() ![]() |
Solution![]() 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. ConstructorThe 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) { } DestructorThe 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.
SwapFinally, 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
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)
Note that Swap() supports the strongest exception guarantee of all—namely, 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
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 | ![]() ![]() |