I l@ve RuBoard previous section next section

Solution

graphics/bulb_icon.gif

Let's answer the questions one at a time.

  1. Which technique is better—using StackImpl as a private base class, or as a member object?

    Both methods give essentially the same effect and nicely separate the two concerns of memory management and object construction/destruction.

    When deciding between private inheritance and containment, my rule of thumb is always to prefer the latter and use inheritance only when absolutely necessary. Both techniques mean "is implemented in terms of," and containment forces a better separation of concerns because the using class is a normal client with access to only the used class's public interface. Use private inheritance instead of containment only when absolutely necessary, which means when:

    • You need access to the class's protected members, or

    • You need to override a virtual function, or

    • The object needs to be constructed before other base subobjects[10]

      [10] Admittedly, in this case it's tempting to use private inheritance anyway for syntactic convenience so that we don't have to write "impl_." in so many places.

  2. How reusable are the last two versions of Stack? What requirements do they put on T, the contained type?

    When writing a templated class, particularly something as potentially widely useful as a generic container, always ask yourself one crucial question: How reusable is my class? Or, to put it a different way: What constraints have I put upon users of the class, and do those constraints unduly limit what those users might want to reasonably do with my class?

    These Stack templates have two major differences from the first one we built. We've discussed one of the differences already. These latest Stacks decouple memory management from contained object construction and destruction, which is nice, but doesn't really affect users. However, there is another important difference. The new Stacks construct and destroy individual objects in place as needed, instead of creating default T objects in the entire buffer and then assigning them as needed.

    This second difference turns out to have significant benefits: better efficiency and reduced requirements on T, the contained type. Recall that our original Stack required T to provide four operations:

    • Default constructor (to construct the v_ buffers)

    • Copy constructor (if Pop returns by value)

    • Nonthrowing destructor (to be able to guarantee exception safety)

    • Exception-safe copy assignment (To set the values in v_, and if the copy assignment throws, then it must guarantee that the target object is still a valid T. Note that this is the only T member function that must be exception-safe in order for our Stack to be exception-safe.)

      Now, however, no default construction is needed, because the only T construction that's ever performed is copy construction. Further, no copy assignment is needed, because T objects are never assigned within Stack or StackImpl. On the other hand, we now always need a copy constructor. This means that the new Stacks require only two things of T:

    • Copy constructor

    • Nonthrowing destructor (to be able to guarantee exception safety)

      How does this measure up to our original question about usability? Well, while it's true that many classes have both default constructors and copy assignment operators, many useful classes do not. (In fact, some objects simply cannot be assigned to, such as objects that contain reference members, because they cannot be reseated.) Now even these can be put into Stacks, whereas in the original version they could not. That's definitely a big advantage over the original version, and one that quite a few users are likely to appreciate as Stack gets reused over time.

      Guideline

      graphics/guideline_icon.gif

      Design with reuse in mind.


  3. Should Stack provide exception specifications on its functions?

    In short: No, because we, the authors of Stack, don't know enough, and we still probably wouldn't want to even if we did know enough. The same is true in principle for any generic container.

    First, consider what we, as the authors of Stack, do know about T, the contained type: precious little. In particular, we don't know in advance which T operations might throw or what they might throw. We could always get a little totalitarian about it and start dictating additional requirements on T, which would certainly let us know more about T and maybe add some useful exception specifications to Stack's member functions. However, doing that would run completely counter to the goal of making Stack widely reusable, and so it's really out of the question.

    Next, you might notice that some container operations (for example, Count()) simply return a scalar value and are known not to throw. Isn't it possible to declare these as throw()? Yes, but there are two good reasons why you probably wouldn't.

    • Writing throw() limits you in the future in case you want to change the underlying implementation to a form that could throw. Loosening an exception specification always runs some risk of breaking existing clients (because the new revision of the class breaks an old promise), so your class will be inherently more resistant to change and therefore more brittle. (Writing throw() on virtual functions can also make classes less extensible, because it greatly restricts people who might want to derive from your classes. It can make sense, but such a decision requires careful thought.)

    • Exception specifications can incur a performance overhead whether an exception is thrown or not, although many compilers are getting better at minimizing this. For widely-used operations and general-purpose containers, it may be better not to use exception specifications in order to avoid this overhead.

I l@ve RuBoard previous section next section