📄 sutter.htm
字号:
Before reading on, stop and think about this class and consider: What access specifier would you write in place of the comment "/*????*/"? And how exactly might you use a class like this to simplify Stack? (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++ see Items E40 and Item E42.)Technique 1: Private Base ClassThe 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 (see Item E42), 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 can even be called. So far, so good.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, which just sets the stack's state to empty and optionally performs an initial allocation. The only operation here which 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 and there will never have been a Stack object at all, so any initial allocation failures in the base class don't affect Stack.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.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. Stack(const Stackother) : StackImpl<T>(other.vused_){ while( vused_ < other.vused_ ) { construct( v_+vused_, // see Sidebar 1 for other.v_[vused_] ); // info on construct ++vused_; }}Copy construction now becomes efficient and elegant. 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 cleanup code inside each one. Stackoperator=(const Stackother) { Stack temp(other); // does all the work Swap( temp ); // this can't throw see Sidebar 1 // for info on swap return *this;}Copy assignment is even more elegant, if a little subtle: we 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. Also, when operator= is made exception-safe like this, a side effect is that it usually also automatically handles self-assignment (e.g., Stack s; s = s;) correctly without further work. (Because self- assignment is exceedingly rare, I omitted the traditional "if( this != )" test which has its own subtle problems. See Guru of the Week #11 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 fully exception-neutral. 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 built-ins), and we're done.Note how much more elegant this is than the exception-safe copy assignment we implemented earlier! This version also requires much less care to ensure that it's been made properly exception-safe. size_t Count() const { return vused_;}Yes, Count is still the easiest member function to write. void Push( const Tt ) { 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. If we were supporting iterators into this container, for instance, they would never be invalidated (by a possible internal grow operation) if the insertion is not completely successful. TTop() { if( vused_ == 0) { throw "empty stack"; } return v_[vused_-1];}The Top function hasn't changed at all. void Pop() { if( vused_ == 0) { throw "pop from empty stack"; } else { --vused_; destroy( v_+vused_ ); // see Sidebar 1 for info } // on destroy }}Neither has Pop, save the new call to destroy.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 cleanup code, whereas last time each constructor would have had to know about the cleanup 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 that 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?)Technique 2: Private MemberNext, 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 (see Item E40) 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 subobjects are created 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.This implementation of Stack is only slightly different from the above. For example, Count returns impl_.vused_ instead of just an inherited vused_. See Listing 1 for the complete code.Which Technique Is Better?So, how do you choose between using StackImpl as a private base class or as a member object? After all, both 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 to always 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' public interface. Use private inheritance instead of containment only when absolutely necessary, which means when either: you need access to the class's protected members; you need to override a virtual function; or the object needs to be constructed before other base subobjects.Admittedly, in this case it's tempting to use private inheritance anyway for syntactic convenience so that we wouldn't have to write "impl_." in so many places.(In the special case of an empty class, private inheritance may allow for the generation of more compact objects. See Scott Meyers' article, Counting Objects in C++, for details.)Relaxing the Requirements on TWhen 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? That is, 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 one we originally considered. We've discussed one already: They 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. Our original Stacks from last time 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 unchanged; note that this is the only T member function which 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 (see Item M4). (In fact, some objects simply cannot be assigned to, such as objects that contain reference members because these 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.Should Stack Provide Exception Specifications?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: The answer is, 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 fascist 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 (e.g., 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 which could throw. (See Item M32 for a discussion of worrying about what may happen in the future.) Loosening an exception specification always runs some risk of breaking existing clients (because the new revision of the class breaks an old promise), and 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 (see Item M15).Destructors That Throw and Why They're EvilThis brings us to our last topic, namely the innocent-looking delete[] p;. What does it really do? And how safe is it?First, recall our standard destroy helper function (see Sidebar 1): template <class FwdIter>void destroy( FwdIter first, FwdIter last ) { while( first != last ) { destroy( first ); // calls "*first"'s dtor ++first; }}This was safe in our example above because we required that T destructors never throw. But what if a contained object's destructor were allowed to throw? Well, consider what happens if destroy is passed a range of five objects: If the first destructor throws, then as it is written now destroy will exit and the other four objects will never be destroyed! This is obviously not a good thing."Ah," you might interrupt, "but can't we clearly get around that by writing destroy to work properly in the face of T's whose destructors are allowed to throw?" Well, that's not as clear as you might think. For example, you'd probably start writing something like this: template <class FwdIter>void destroy( FwdIter first, FwdIter last ) { while( first != last ) { try { destroy( first ); } catch(...) { /* what goes here? */ } ++first;
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -