📄 reeves.htm
字号:
v_[top_] = element;top_++;We deliberately moved the incrementing of top_ after the possible exception from the assignment operator. We could have written something like: try { v_[top_++] = element;} catch (...) { top_--; // reset state throw;}This is similar to what had to be done in the pop() function. In that case we did not have any choice; in push() we do. Goal V says that if we can arrange our functions so that we can avoid writing try/catch blocks then we should do so. It leaves us with cleaner code and it is more efficient to let the exception just propagate than it is to catch and rethrow it.There is a potential downside with doing this that has to be noted when we write a try/catch block, it is obvious that there is the possibility of an exception. When we write: v_[top_] = element;top_++;instead of v_[top_++] = element;it is not clear that the first form is necessitated by the possibility of an exception. We run the risk that during maintenance, some experienced C programmer (but new C++ programmer) will take a look at the two lines and decide that whoever wrote them did not appreciate the compactness of expression possible in C++ and change them back into the latter. In some sense, this is one of the most annoying drawbacks to using exceptions in C++ a great many of the cherished idioms from C are going to have to be discarded. Instead, we are going to have to use a much more deliberate (and verbose) style that allows us to maintain better control over the state of our objects in the presence of exceptions.In order to avoid this maintenance problem, we must document every place in our code where we think an exception is possible, and especially everywhere we have changed code to make it exception safe. I use comments of the form //>x. This is not a particularly good solution, but it is better than nothing.Guideline 8. Always use a catch(...) block to cope with propagating exceptions. If we follow Guideline 7 we will avoid catching exceptions if we can, but there will be times when it can not be avoided, or the work necessary to avoid it is excessive. If we conclude that an exception might occur, and that it could leave us in a bad state, then we want to make sure we catch that exception. Since we are not concerned with handling the exception, only with protecting the state of the object, then the type of the exception does not matter. Therefore we use a catch block with a single handler: catch (...) { /* ... */ }This is pretty obvious for a template class like Stack, where we have no idea what kind of exception might be thrown by T, but in general, even if we know (or think we know) what the actual exception type is, if we are not actually handling the exception, then we should use a catch (...) clause.Guideline 9. If you get stuck, call terminate(). This is not really about handling exceptions, but it does have something to do with coping with errors. There are places where throwing an exception does not make sense, and the only possibility left is to end the program. An obvious circumstance where this happens is in a user defined version of unexpected_handler. Unexpected_handler must either throw an acceptable exception, or end the program.Before exceptions, the normal way to abnormally terminate a program was by calling the C library function abort(). In the new C++ world, we should call terminate() instead. terminate() just calls terminate_handler. In the default case, this calls abort(), but the user can replace the default terminate_handler with a program specific version. So call terminate() to allow any user defined terminate_handler to run.Goal VI. Do not hide exception information from other parts of the program that might need it. Just to reiterate the obvious: the purpose of exceptions is to pass information from the point where an error is detectable to a point where the error can be handled. If you throw a different exception rather than rethrowing the original exception, you want to make sure you are increasing the information content by doing so.Guideline 10. Always rethrow the exception caught in a catch (...) clause. I will predict that this will probably be the most common exception handling mistake. Once you have reset your state in a catch (...) clause, you want to be sure that you rethrow the exception so that higher level routines will have a chance to handle it. A failure to rethrow the exception means you have "handled" the exception which is probably not what happened at all. Just for a reminder, the statement to rethrow an exception is: throw; // see Item M12 for detailsGuideline 11. Rethrow a different exception only to provide additional information or capability. If forgetting to rethrow the exception from a catch (...) clause is likely to be the most common exception handling mistake, rethrowing a different exception will probably be second. Consider the following: void foo() try{ // do something}catch (PrivateExceptionTypeex) { // clean up}catch (...) { // clean up throw exception("Unknown exception in foo");}At first reading this seems to make sense. Foo() invokes some operation that might throw a private exception type, which is caught and handled. To be on the safe side, foo() includes a catch-all block just to cover the bases. The problem is that when foo() throws the generic exception in the catch-all block, it effectively destroys all the information in the original exception. This information presumably indicated what the original error was and might have been of use to a higher level routine that was willing and prepared to deal with an error of that type. Now, all that propagates upward is a self-fulfilling prophecy, and the program will probably terminate because nobody knows how to handle an "unknown exception".In our ongoing Stack example, there are several places where we use the new statement. Under the C++ Standard, new will throw a bad_alloc exception if the memory allocation fails. In the second part of this series of articles [Reference 3], we add a private exception class to Stack that is derived from bad_alloc. In Stack<T>::AllocationError we include some additional information besides that available from bad_alloc alone, including the new size of the stack being allocated.In order to be able to throw Stack<T>::AllocationError we use the nothrow() placement form of new (see Item E7) as in: new_buffer = new (nothrow()) T[new_nelems];if (new_buffer == null) throw Stack::AllocationError(new_nelems);Class nothrow is defined in the Standard header file <new> along with the placement form of operator new used above. When this form is used, operator new reverts to the traditional behavior and returns a null pointer if the allocation fails. This is the preferred way to do this. Note that this form only eliminates the possible exception from operator new, an exception can still be thrown by the constructor of the object in the new expression. For that reason, we want to be careful NOT to do the following: try { new_buffer = new T[new_nelems];} catch (...) { // catch bad_alloc from new throw Stack<T>::AllocationError(new_nelems);}In this case, our catch (...) clause will not only catch the bad_alloc exception as the (erroneous) comment indicates, but we will also catch any exceptions thrown by the T::T() constructor. If the new expression fails because of an exception in the latter, we will hide that exception with our version of bad_alloc. While it is true that either case will indicate that the function failed, hiding the original exception means that any higher level routine that handles the Stack<T>::AllocationError exception will be solving the wrong problem.Guideline 12. Make sure one catch block does not hide another. When the exception runtime attempts to locate a handler for an exception, it tries the handlers in order. Unlike function overloading, where the best match from a set of possible functions is found, an exception handler is found on a first match basis (see Item M12). This means that you always want to put more specific handlers before more general handlers. Handlers specifying base classes (or references to same) must come after those for more derived classes. Obviously, any catch (...) clause must be the last handler of a sequence. The compiler will almost certainly complain if the catch (...) clause is not the last one, but it may not generate an error message for other incorrect orderings, so beware.Goal VII. Never depend upon destructors for functionality in any situation where fault-tolerance is required. As a general rule, we do not want exceptions propagating from a destructor (see Item M11). There are two reasons for this. First, there is simply a semantic problem with deciding what it means for a destructor to have an exception. For ordinary functions, an exception typically means the function failed. What does it mean when a destructor fails? Certainly, it should not mean that the object is left in a usable state since it was not going to be usable if the destructor succeeded. In most cases the memory for the object will disappear anyway, whether the destructor succeeds or not. In other words, there is not much we can do about handling an error that occurs in a destructor, and we ordinarily want the destructor to finish anyway, so there is not much point in signaling errors from a destructor, even though exceptions give us a way to do this.The second reason for avoiding exceptions in destructors is more fundamental - they are likely to abort the program. In a sense, destructors are part of the exception handling mechanism itself. Destructors are invoked by the exception handling runtime as part of the stack unwind process. If a destructor throws an exception during an exception stack unwind, then the Standard says the exception handling mechanism gives up and calls terminate(). For this reason, other writers have taken the position that destructors should never throw or propagate exceptions [Reference 2, Reference 5]. I do not go quite that far, but the following guidelines should apply under most circumstances.Guideline 13. Do not throw exceptions from a destructor body. This is the principal behind Goal V again. Clearly, there is no sense in testing for error conditions in a destructor body unless they can be handled right then and there otherwise ignore them (or call terminate()).Guideline 14. Do not arbitrarily handle all exceptions propagating from a destructor. Exceptions can still legitimately occur when a destructor calls an ordinary function that throws an exception. It is tempting to suggest that destructors should not call such functions, or to say that every destructor should handle its own exceptions tempting, but unrealistic. The first is impractical, and the second leads to constructs such as catch (...) {}which I object to on aesthetic grounds. On the other hand, are there ever legitimate occasions when we might want to propagate an exception from a destructor? Consider the following real-world problem:A program captures medical images from a Computed Radiography (CR) scanner and transfers them over a fiber optic network to a central image server.If there is any problem with the network, or with the image server, the program is required to save the image to local disk, generate a message on both the local console and the system administrator's console (a separate network is available for system administration tasks), and enter an error state that prevents any more images from being captured until the problem is fixed and the image saved on disk is transferred to the central server. This is necessary because the X-ray plates used by a CR machine are erased by the scan once scanned, the image must be captured or it is lost forever. (This program exists, and is written in C++. It was written before exceptions were available, and I doubt it has been updated. The process of doing so would make an interesting test case (see Reference 6)).Assume the network connection is encapsulated as an object (call it an endpoint). If the endpoint destructor is called, and the connection is still open, the destructor will attempt to close it. If some error occurs it seems reasonable, given the nature of this program, that an exception should be thrown to indicate a network problem to higher levels.This would seem like a perfect case for having a destructor throw an exception, but maybe not. A closer look at this program reveals that under normal circumstances the network connection should always be closed before the endpoint is destroyed. If the connection is still open when the destructor is invoked, it probably means the endpoint is being destroyed as a result of some other exception. If this is the case, then the last thing we want to do is throw another exception. In fact, we may be trying to destroy the endpoint as a result of a stack unwind that resulted from an exception thrown by another network function call. We want this exception to propagate.So back to the question of whether a destructor should handle all possible exceptions or allow them to propagate. Destructors being the type of functions they are, it may make sense to ignore a lot of errors in destructors (i.e. catch the exceptions and not rethrow them). Nevertheless, at some point in coding a class or library, we have to acknowledge that our users will probably have a better view of the big picture than we do. There has to be a limit on how much we try to handle at a given level. This is especially true for destructors. The person who uses our classes has to accept some responsibility for how they are used. This means that any program that has to be fault tolerant must be designed and coded with special care regarding the types of exceptions it has to guard against.If our medical imaging program is written correctly, then it will not depend upon destructors for normal functionality. The remote file will always be closed before the file object is destroyed; the network connection will always be closed before the network object is destroyed, etc. Exceptions are possible in fact the program may depend upon them but their occurrence will be anticipated as part of normal design, and they will be handled outside of the destructors. We are left with something of a paradox: the more fault tolerant a program has to be, the less likely it is going to have to worry about exceptions from destructors.As noted above, I object to constructs such as catch (...) {}on aesthetic grounds alone, but there is a practical aspect as well. Templates are but one example of a case where we have no idea what kinds of exceptions are possible from the operations being invoked on the objects used to instantiate the template. There are times when we have to assume that the user knows what she is doing and stay out of the way. This goes even for exceptions propagating from destructors.Goal VIII. Do not get too paranoid. As a final point, I have noted that it is possible to get paranoid when trying to deal with exceptions. Maybe this is only a problem for me because I have spent so much time lately worrying about exceptions. Still, I am willing to bet that other people are also going to run into this.
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -