📄 reeves.htm
字号:
throw; // redundant}This example uses several features from the new Standard, so do not expect this to work on your compiler yet. Placing the try keyword immediately after the parameter list (or the exception specification if one exists), and the catch clauses after the function body, produces a function try block. A function try block associates a handler with the entire function body, including the constructor initializer list. In this example, the initializer list is not the concern, but you can see that the initializer list is within the scope of the try. With any pointer not set in the constructor body guaranteed to be null, the catch block can safely invoke delete on them. (The throw in the catch block is redundant. A catch clause of a function try block for a constructor or a destructor will automatically rethrow the exception when it finishes. As a matter of style, however, I always explicitly rethrow exceptions caught by a catch (...) clause (Guideline 10).) All this leads to:Guideline 6: If you have raw data pointers as members, initialize them to null in the initializer list of your constructor(s), then do necessary allocations in the constructor body where a catch block can deal with potential resource leaks. This is one possible way to deal with a potential resource leak in a constructor. Another technique is to use the "resource acquisition is initialization" strategy. In this case, we make sure that every resource is associated with an object whose destructor will deallocate it. For dealing with memory allocated from the free store, the standard library template class auto_ptr (Listing 2) is available. Applied to our example: class X { auto_ptr<T1> ap1_; auto_ptr<T2> ap2_;public: X(); ~X() {};};X::X() : ap1_(new T1), ap2_(new T2){}Alternatively: X::X(){ ap1_.reset(new T1); ap2_.reset(new T2);}Now, since ap1_ and ap2_ are both objects, if an exception occurs trying to initialize ap2_, then the stack unwind will destroy ap1_, which will call delete on the allocated pointer. In this case, our destructor is empty since the destructors of member objects are invoked automatically.Besides having a destructor that will delete the resource, template auto_ptr also provides functionality for safely transferring ownership of a resource. We have made use of this capability several times already. In this case, we can use an "acquire then transfer ownership" strategy to give us the following version of X's constructor: class X { T1* p1_; T2* p2_;public: X(); ~X() {delete p1_; delete p2_;}};X::X() : p1_(), p2_(){ auto_ptr<T1> t1(new T1); auto_ptr<T2> t2(new T2); p1_ = t1.release(); p2_ = t2.release();}The auto_ptr objects are used to acquire the resources in a manner that guarantees they will be deleted if an exception occurs. When all resources have been successfully acquired, ownership is transferred to the class itself. This is just Guideline 2 applied to resource acquisition. I see this as a transitional strategy for constructors, however. In the long run, I suspect that the use of raw data pointers as class members will diminish in favor of auto_ptr style objects. This simplifies maintenance as well as the problems of coping with exceptions. If you worry about performance, keep in mind that all the operations of auto_ptr are inline functions (see Item E33). Most of these are one line functions that any decent compiler should have no trouble handling.Resource allocations in ordinary functions can also cause resource leaks. Whereas a constructor is building an object and the goal is to make sure everything already acquired is released if an exception occurs, a function usually obtains a resource for internal use and releases it upon completion. If an exception happens after the resource is acquired but before it is released, then we have a leak. Under the discussion for Guideline 2, we used an auto_array_ptr object in Stack::push() to manage the new buffer while it was being initialized. We can do the same thing in Stack::operator=(): template<class T>Stack<T>Stack::operator=(const Stack<T>rhs){ if (this == return *this; auto_array_ptr<T> new_buffer = new T[rhs.nelems_]; //x1 for (int i = 0; i < rhs.top_; i++) new_buffer[i] = rhs.v_[i]; //x2 v_ = new_buffer.reset(v_); // swap ownership nelems_ = rhs.nelems_; top_ = rhs.top_; return *this;}If v_ were an auto_array_ptr object instead of a raw pointer we would use the Standard template function swap() to perform the exchange of ownership: swap(v_, new_buffer);Finally, we can have resource leaks in destructors. As a general rule, we do not want to throw (or propagate) exceptions from destructors (see Item M11 and Herb Sutter's article on "Exception-Safe Generic Containers"). Nevertheless, we can not always prevent it, so let us take a look at a simple problem. Consider our first example of class X above. We have two pointers to two different types of objects. Everything has gone well, and now the destructor of X is invoked. It is pretty simple. X::~X() { delete p2_; delete p1_; }Not much to go wrong here, but assume that it does say the T2 object throws an exception when deleted. Like constructors, destructors are special functions for the exception runtime mechanism. When an exception occurs in a destructor, it is treated like an exception in a constructor, i.e. all complete subobjects that still exist are destroyed (in reverse order of their construction) and then the memory deallocation function is called, if needed. (I assume this is what is suppose to happen. The Standard is not at all clear about how exceptions from destructors are handled. I make this assumption based upon the discussion of exceptions in the ARM (see Item E50), and because it seems the logical thing to do. It would not surprise me if compiler implementers disagree.)Exactly the same problem occurs here as it does in a constructor. The exception from the destructor of T2 will terminate the body of the destructor without deleting the resource held by p1_. The solutions are likewise similar to those applied to a constructor.For example, we can attempt to catch the exception and guarantee that other resources are deleted. Unlike the constructor however, there is no way to organize things so we can cover everything in a single catch clause at least not without more work than is worth it. Once again, we turn back to the "acquisition is initialization" strategy and use auto_ptr objects. If all our member variables are auto_ptr objects then our destructor is empty anyway, so let us assume otherwise. X::~X(){ auto_ptr t1(p1_); auto_ptr t2(p2_);}This may seem a little silly, but it does work. We transfer ownership of the resources to the two temporary objects (we create them in the same order that p1_ and p2_ are declared so they will be destroyed in reverse order). When the destructor body exits, these objects are destroyed, deleting their pointers. If an exception occurs in the destruction of t2, then the stack unwind will still destroy t1.Goal V. Do not catch any exception you do not have to. There is an old C rule of error handling (actually it probably goes back to the Countess Ada Lovelace) that states: "do not test for any error condition you do not know how to handle." Like most cynical proverbs, there is a certain amount of wisdom in this. In C++, we now have a standard way to handle any error condition throw an exception but this just shifts the burden of the problem. The basic truth remains: it is a waste of time (yours and the computer's) to catch an exception you do not know how to handle.Guideline 7: Rewrite functions to preserve state, if possible. As we have seen in the discussions above, when an exception propagates upward from a lower level function, we may have to catch it just to reset the state of our object. The point of Goal V is that we want to avoid this as much as we can. Consider our rewritten push() function. The last two lines now read:
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -