?? sutter.htm
字號:
Exception-Safe Generic Containers. By Herb Sutter This is an updated version of an article that appeared in the September and November-December 1997 issues of the C++ Report.Exception-Safe Generic Containersby Herb SutterException handling and generic programming are two of C++'s most powerful features. Both, however, require exceptional care, and writing an efficient reusable generic container is nearly as difficult as writing exception-safe code.This article tackles both of these major features at once, by examining how to write exception-safe (works properly in the presence of exceptions) and exception-neutral (propagates all exceptions to the caller) generic containers. That's easy enough to say, but it's no mean feat. If you have any doubts on that score, see Tom Cargill's excellent article, Exception Handling: A False Sense of Security.This article begins where Cargill's left off, namely by presenting an exception-neutral version of the Stack template he critiques. In the end, we'll significantly improve the Stack container by reducing the requirements on T, the contained type, and show advanced techniques for managing resources exception-safely. Along the way we'll find the answers to questions like the following: What are the different "levels" of exception safety? Can or should generic containers be fully exception-neutral? Are the standard library containers exception-safe or -neutral? Does exception safety affect the design of your container's public interface? Should generic containers use exception specifications?The Stack ContainerHere is the declaration of the Stack template, substantially the same as in Cargill's article. Our mission: to make Stack exception-neutral. That is, Stack objects should always be in a correct and consistent state regardless of any exceptions that might be thrown in the course of executing Stack's member functions, and if any exceptions are thrown they should be propagated seamlessly through to the caller, who can deal with them as he pleases because he knows the context of T and we don't. template <class T> class Stack {public: Stack(); ~Stack(); Stack(const Stack Stackoperator=(const Stack size_t Size() const; void Push(const T T Pop(); // if empty, throws exceptionprivate: T* v_; // ptr to a memory area big size_t vsize_; // enough for 'vsize_' T's size_t vused_; // # of T's actually in use};Before reading on, stop and think about this container and consider: What are the exception-safety issues? How can this class be made exception-neutral, so that any exceptions are propagated to the caller without causing integrity problems in a Stack object?Default ConstructionRight away, we can see that Stack is going to have to manage dynamic memory resources. Clearly one key is going to be avoiding leaks even in the presence of exceptions thrown by T operations and standard memory allocations. For now, we'll manage these memory resources within each Stack member function. Later, we'll improve on this by using a private base class (see Item E42) to encapsulate resource ownership.First, consider one possible default constructor: template<class T>Stack<T>::Stack() : v_(0), vsize_(10), vused_(0) // nothing used yet{ v_ = new T[vsize_]; // initial allocation}Is this constructor exception-safe? To find out, consider what might throw. In short, the answer is: "Any function." So the first step is to analyze this code and determine what functions will actually be called, including both free functions and constructors, destructors, operators, and other member functions.This Stack constructor first sets vsize_ to 10, then attempts to allocate some initial memory using new T[vsize_]. The latter first tries to call operator new[] (either the default operator new[] or one provided by T see Item M8) to allocate the memory, then tries to call T::T a total of vsize_ times. There are two operations that might fail: first, the memory allocation itself, in which case operator new[] will throw a bad_alloc exception; and second, T's default constructor, which might throw anything at all, and in which case any objects that were constructed are destroyed and the allocated memory is automatically guaranteed to be deallocated via operator delete[]. (See Item M8 and the sidebar to Scott Meyers' article, Counting Objects in C++.)Hence the above function is fully exception-safe, and we can move on to the next ...... what? Why is it exception-safe, you ask? All right, let's examine it in a little more detail: We're exception-neutral. We don't catch anything, so if the new throws then the exception is correctly propagated up to our caller as required. We don't leak. If the operator new[] allocation call exited by throwing a bad_alloc exception, then no memory was allocated to begin with so there can't be a leak. If one of the T constructors threw, then any T objects that were fully constructed were properly destroyed and finally operator delete[] was automatically called to release the memory. That makes us leak-proof, as advertised. (I'm ignoring for now the possibility that one of the T destructor calls might throw during the cleanup, which would call terminate() and simply kill the program altogether and leave events well out of your control anyway. See below for more information on Destructors That Throw and Why They're Evil.) We're in a consistent state whether any part of the new throws or not. Now, you might think that if the new throws, then vsize_ has already been set to 10 when in fact nothing was successfully allocated. Isn't that inconsistent? Not really, because it's irrelevant. Remember, if the new throws we propagate the exception out of our own constructor, right? And, by definition, "exiting a constructor by means of an exception" means our Stack proto-object never actually got to become a completely constructed object at all, its lifetime never started, and hence its state is meaningless because the object never existed. It doesn't matter what the memory that briefly held vsize_ was set to, any more than it matters what the memory was set to after we leave an object's destructor. All that's left is raw memory, smoke and ashes.All right, I'll admit it... I put the new in the constructor body purely to open the door for that last #3 discussion. What I'd actually prefer to write is: template<class T>Stack<T>::Stack() : v_(new T[10]), // default allocation vsize_(10), vused_(0) // nothing used yet{ }Both versions are practically equivalent. I prefer the latter because it follows the usual good practice of initializing members in initializer lists whenever possible (see Item E12).DestructionThe destructor looks a lot easier, once we make a (greatly) simplifying assumption: template<class T>Stack<T>::~Stack() { delete[] v_; // this can't throw }Why can't the delete[] call throw? Recall that this invokes T::~T for each object in the array, then calls operator delete[] to deallocate the memory (see Item M8). Now, we know that the deallocation by operator delete[] may never throw, because its signature is always one of the following: void operator delete[]( void* ) throw();void operator delete[]( void*, size_t ) throw();Strictly speaking, this doesn't prevent someone from providing an overloaded operator delete[] that does throw, but any such overload would violate this clear intent and should be considered defective. Hence the only thing that could possibly throw is one of the T::~T calls, and we're arbitrarily going to have Stack require that T::~T may not throw. Why? To make a long story short, we just can't implement the Stack destructor with complete exception safety if T::~T can throw, that's why. However, requiring that T::~T may not throw isn't particularly onerous, because there are plenty of other reasons why destructors should never be allowed to throw at all. (Frankly, you won't go far wrong if you just habitually write throw() after the declaration of every destructor you ever write. Even if exception specifications cause expensive checks under your current compiler (see Item M15), at least write all your destructors as though they were specified as throw()... that is, never allow exceptions to leave destructors.) Any class whose destructor can throw is likely to cause you all sorts of other problems anyway sooner or later, and you can't even reliably new[] or delete[] an array of them. More on that later.Copy Construction and Copy AssignmentThe next few functions will use a common helper function, NewCopy, to manage allocating and growing memory. NewCopy takes a pointer to (src) and size of (srcsize) an existing T buffer, and returns a pointer to a new and possibly larger copy of the buffer, passing ownership of the new buffer to the caller. If exceptions are encountered, NewCopy correctly releases all temporary resources and propagates the exception in such a way that nothing is leaked. template<class T>T* NewCopy( const T* src, size_t srcsize, size_t destsize ) { assert( destsize = srcsize ); T* dest = new T[destsize]; try { copy( src, src+srcsize, dest ); // copy is part of the STL; // see Item M35 } catch(...) { delete[] dest; // this can't throw throw; // rethrow original exception } return dest;}Let's analyze this one step at a time: In the new statement, the allocation might throw bad_alloc or the T::T's may throw anything. In either case, nothing is allocated and we simply allow the exception to propagate. This is leak-free and exception-neutral. Next, we assign all the existing values using copy, and copy invokes T::operator=. If any of the assignments fail, we catch the exception, free the allocated memory, and rethrow the original exception. This is again both leak-free and exception-neutral. However, there's an important subtlety here: T::operator= must guarantee that, if it does throw, then the assigned-to T object must be unchanged. (Later, I will show an improved version of Stack which does not rely on T::operator=.) If the allocation and copy both succeeded, then we return the pointer to the new buffer and relinquish ownership (that is, the caller is responsible for the buffer from here on out). The return simply copies the pointer value, which cannot throw.With NewCopy in hand, the Stack copy constructor is easy to write: template<class T>Stack<T>::Stack( const Stack<T>other ) : v_(NewCopy( other.v_, other.vsize_, other.vsize_ )), vsize_(other.vsize_), vused_(other.vused_){ }The only possible exception is from NewCopy, which manages its own resources. Next, we tackle copy assignment: template<class T>Stack<T>Stack<T>::operator=( const Stack<T>other ) { if( this != ) { T* v_new = NewCopy( other.v_, other.vsize_, other.vsize_ ); delete[] v_; // this can't throw v_ = v_new; // take ownership vsize_ = other.vsize_; vused_ = other.vused_; } return *this; // safe, no copy involved}Again, after the routine weak guard against self-assignment (see Item E17), only the NewCopy call might throw; if it does, we correctly propagate that exception without affecting the Stack object's state. To the caller, if the assignment throws then the state is unchanged, and if the assignment doesn't throw then the assignment and all of its side effects are successful and complete.Size(), Push(), and Pop()The easiest of all Stack's members to implement safely is Size, because all it does is copy a built-in which can never throw: template<class T>size_t Stack<T>::Size() const { return vused_; // safe, builtins don't throw}However, with Push we need to apply our now-usual duty of care: template<class T>void Stack<T>::Push( const Tt ) { if( vused_ == vsize_ ) // grow if necessary { // by some grow factor size_t vsize_new = vsize_*2+1;
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -