?? sutter.htm
字號:
++first; }}The tricky part is the "what goes here?". There are really only three choices: either the catch body rethrows the exception, or it converts the exception by throwing something else, or it throws nothing and continues the loop. If the catch body rethrows the exception, then the destroy function nicely meets the requirement of being exception-neutral, because it does indeed allow any T exceptions to propagate out normally. However, it still doesn't meet the safety requirement that no resources be leaked if exceptions occur. Because destroy has no way of signaling how many objects were not successfully destroyed, those objects can never be properly destroyed and so any resources associated with them will be unavoidably leaked. Definitely not good. If the catch body converts the exception by throwing something else, we've clearly failed to meet both the neutrality and the safety requirements. Enough said. If the catch body does not throw or rethrow anything, then the destroy function nicely meets the safety requirement that no resources be leaked if an exception is thrown. (True, if a T destructor could throw in a way that its resources might not be completely released, then there could still be a leak. However, this isn't destroy's problem... this just means that T itself is not exception-safe, but destroy is still properly leak-free in that it doesn't fail to release any resources that it should (namely the T objects themselves).) However, obviously it fails to meet the neutrality requirement that T exceptions be allowed to pass through because exceptions are absorbed and ignored (as far as the caller is concerned, even if the catch body does attempt to do some sort of logging).I've seen people suggest that the function should catch the exception and "save" it while continuing to destroy everything else, then rethrow it at the end. That too isn't a solution; for example, it can't correctly deal with multiple exceptions should multiple T destructors throw (even if you save them all until the end, you can only end by throwing one of them and the others are silently absorbed). You might be thinking of other alternatives, but trust me, they all boil down to writing code like this somewhere because you have a set of objects and they all need to be destroyed. Someone, somewhere, is going to end up writing non-exception-neutral code (at best) if T destructors are ever allowed to throw......Which brings us to the innocent-looking new[] and delete[].The issue with both of these is that they have fundamentally the same problem we just described for destroy! For example, consider the following code: T* p = new T[10];delete[] p;Looks like normal harmless C++, doesn't it? But have you ever wondered what new[] and delete[] do if a T destructor throws? Even if you have wondered, you can't know the answer for the simple reason that there is none: The standard says you get undefined behavior if a T destructor throws anywhere in this code, which means that any code that allocates or deallocates an array of objects whose destructors could throw can result in undefined behavior. This may raise some eyebrows, so let's see why this is so:First, consider what happens if the constructions all succeed, and then during the delete[] operation the fifth T destructor throws. Then delete[] has the same catch-22 problem (pun intended) outlined above for destroy: It can't allow the exception to propagate because then the remaining T objects would be irretrievably undestroyable, but it also can't translate or absorb the exception because then it wouldn't be exception-neutral.Second, consider what happens if the fifth constructor throws. Then the fourth object's destructor is invoked, then the third's, and so on until all the T objects that were successfully constructed have again been destroyed, and the memory is safely deallocated. But what if things don't go so smoothly? In particular, what if, after the fifth constructor throws, the fourth object's destructor throws? And, if that's ignored, the third's? You can see where this is going.If destructors may throw, then neither new[] nor delete[] can be made exception-safe and exception-neutral.The bottom line is simply this: Don't ever write destructors that can allow an exception to escape. (The C++ standard makes the blanket statement: "No destructor operation defined in the C++ Standard Library will throw an exception." Not only do all of the standard classes have this property, but in particular it is illegal to instantiate a standard container with a type whose destructor does throw.) If you do write a class with such a destructor, you will not be able to safely even new[] or delete[] an array of those objects. All destructors should always be implemented as though they had an exception specification of throw()... that is, no exceptions must ever be allowed to propagate.Granted, some may feel that this state of affairs is a little unfortunate, because one of the original reasons for having exceptions was to allow both constructors and destructors to report failures (because they have no return values). This isn't quite true, because the intent was mainly for constructor failures (after all, destructors are supposed to destroy, so the scope for failure is definitely less). The good news is that exceptions are still perfectly useful for reporting construction failures, including array and array-new[] construction failures, because there they can work predictably even if a construction does throw.Safe ExceptionsThe advice "be aware, drive with care" certainly applies to writing exception-safe code for containers and other objects. To do it successfully, you do have to meet a sometimes significant extra duty of care, but don't get unduly frightened by exceptions. Apply the guidelines outlined above that is, isolate your resource management, use the "update a temporary and swap" idiom, and never write classes whose destructors can allow exceptions to escape and you'll be well on your way to safe and happy production code that is both exception-safe and exception- neutral. The advantages can be both concrete and well worth the trouble for your library and your library's users.AcknowledgmentsThanks to Dave Abrahams and Greg Colvin for their comments. Dave and Greg are, with Matt Austern, the authors of the two complete committee proposals for adding these exception safety guarantees into the standard library (see Sidebar 2). Sidebar 1: Some Standard Helper Functions The Stack and StackImpl presented in this article use three helper functions from the standard library (see Item E49): construct, destroy, and swap. In simplified form, here's what these functions look like: // construct() constructs a new object in // a given location using an initial value // template <class T1, class T2> void construct( T1* p, const T2value ) { new (p) T1(value); }This is called "placement new," and instead of allocating memory for the new object it just puts it into the memory pointed at by p (see Item M8). Any object new'd in this way should generally be destroyed by calling its destructor explicitly (as in the following two functions), rather than using delete (again, see Item M8). // destroy() destroys an object or a range // of objects // template <class T> void destroy( T* p ) { p-~T(); } template <class FwdIter> void destroy( FwdIter first, FwdIter last ) { while( first != last ) { destroy( first ); ++first; } } // swap() just exchanges two values // template <class T> void swap( Ta, Tb ) { T temp(a); a = b; b = temp; }Of these, destroy(iter,iter) is the most interesting. We'll return to it a little later in the main article; it illustrates more than you might think!To find out more about these standard functions, take a few minutes to examine how they're written in the standard library implementation you're using. It's a very worthwhile and enlightening exercise. Sidebar 2: Exception Safety and the Standard Library Are the standard library containers exception-safe and exception-neutral? The short answer is: Yes. ( Here I'm focusing my attention on the containers and iterators portion of the standard library. Other parts of the library, such as iostreams and facets, are specified to be strongly exception-safe.)All iterators returned from standard containers are exception-safe and can be copied without throwing an exception.All standard containers must implement the basic guarantee for all operations: They are always destructible, and they are always in a consistent (if not predictable) state even in the presence of exceptions. To make this possible, certain important functions are required not to throw including swap (the importance of which was illustrated by my second example), allocator::deallocate (the importance of which was illustrated by the discussion of ::operator delete in the first example) and certain operations of the template parameter types themselves (especially the destructor, the importance of which is illustrated under the subhead "Destructors That Throw and Why They're Evil").All standard containers must also implement the strong guarantee for all operations (with two exceptions; see next paragraph): They always have commit-or-rollback semantics, so that an operation such as an insert either succeeds completely or else does not change the program state at all. "No change" also means that failed operations do not affect the validity of any iterators that happened to be already pointing into the container.There are only two exceptions: First, for all containers, multi-element inserts ("iterator range" inserts) are never strongly exception-safe. Second, for vector<T> and deque<T> only, inserts and erases (whether single- or multi-element) are strongly exception-safe only if T's copy constructor or assignment operator do not throw. ( Unfortunately, this means that inserting into and erasing from a vector<string> or a vector<vector<int> >, for example, are not strongly exception-safe.) Why these particular limitations? Because to roll back either kind of operation isn't possible without extra space/time overhead, and the standard did not want to require that overhead in the name of exception safety. All other container operations can be made strongly exception-safe without overhead. So if you ever insert a range of elements into a container, or if T's copy constructor or assignment operator can throw and you insert into or erase from a vector<T> or a deque<T>, the container will not necessarily have predictable contents afterwards and iterators into it may have been invalidated.What does this mean for you? Well, if you write a class that has a container member and you perform range insertions, or that has a member of type vector<T> or deque<T> and T's copy constructor or assignment operator can throw, then you are responsible for doing the extra work to ensure that your own class' state is predictable if exceptions do occur. Fortunately, this "extra work" is pretty simple: Whenever you want to insert into or erase from the container, first take a copy of the container, then perform the change on the copy, and finally use swap to switch over to using that new version after you know that the copy-and-change steps have succeeded. Listing 1 template <class T> class Stack { public: Stack(size_t size=0) : impl_(size) { } Stack(const Stackother) : impl_(other.impl_.vused_) { while( impl_.vused_ < other.impl_.vused_ ) { construct( impl_.v_+impl_.vused_, other.impl_.v_[impl_.vused_] ); ++impl_.vused_; } } Stackoperator=(const Stackother) { Stack temp(other); impl_.Swap(temp.impl_); // this can't throw return *this; } size_t Count() const { return impl_.vused_; } void Push( const Tt ) { if( impl_.vused_ == impl_.vsize_ ) { Stack temp( impl_.vsize_*2+1 ); while( temp.Count() < impl_.vused_ ) { temp.Push( impl_.v_[temp.Count()] ); } temp.Push( t ); impl_.Swap( temp.impl_ ); } else { construct( impl_.v_+impl_.vused_, t ); ++impl_.vused_; } } TTop() { if( impl_.vused_ == 0) { throw "empty stack"; } return impl_.v_[impl_.vused_-1]; } void Pop() { if( impl_.vused_ == 0) { throw "pop from empty stack"; } else { --impl_.vused_; destroy( impl_.v_+impl_.vused_ ); } } private: StackImpl<T> impl_; // private implementation }
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -