Solution
Let's answer the questions one at a time.
-
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
-
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
|
Design with reuse in
mind.
|
-
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.
|