📄 ch06.htm
字号:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN"><HTML><HEAD> <META NAME="Author" Content="Steph Mineart"> <META HTTP-EQUIV="Content-Type" CONTENT="text/html;CHARSET=iso-8859-1"> <TITLE>ANSI/ISO C++ Professional Programmer's Handbook - Chapter 6 - Exception Handling</TITLE> <link rel="stylesheet" TYPE="text/css" href="/includes/stylesheets/ebooks.css"></head><BODY TEXT="#000000" BGCOLOR="#FFFFFF"><CENTER><H1><img src="/publishers/que/series/professional/0789720221/button/que.gif" WIDTH="171" HEIGHT="66" ALIGN="BOTTOM" BORDER="0"><BR>ANSI/ISO C++ Professional Programmer's Handbook</H1></CENTER><CENTER> <P><A HREF="/publishers/que/series/professional/0789720221/index.htm"><img src="/publishers/que/series/professional/0789720221/button/contents.gif" WIDTH="128"HEIGHT="28" ALIGN="BOTTOM" ALT="Contents" BORDER="0"></A> <HR></CENTER><H1 align="center">6</H1><h1 align="center"> Exception Handling </h1><address>by Danny Kalev</address><ul> <li><a href="#Heading1">Introduction</a> <li><a href="#Heading2">Traditional Error Handling Methods</a> <ul> <li><a href="#Heading3">Returning an Error Code</a> <li><a href="#Heading4">Turning on a Global Flag</a> <li><a href="#Heading5">Terminating the Program</a> </ul> <li><a href="#Heading6">Enter Exception Handling</a> <ul> <li><a href="#Heading7">The Challenges of Implementation of Exception Handling</a> </ul> <li><a href="#Heading8">Applying Exception Handling</a> <ul> <li><a href="#Heading9">Exception Handling Constituents</a> <li><a href="#Heading10">Stack Unwinding</a> <li><a href="#Heading11">Passing Exception Objects to a Handler</a> <li><a href="#Heading12">Exception Type Match</a> <li><a href="#Heading13">Exceptions as Objects</a> <li><a href="#Heading14">Exception Specification</a> </ul> <li><a href="#Heading15">Exceptions During Object's Construction and Destruction</a> <ul> <li><a href="#Heading16">Throwing Exceptions From A Destructor is Dangerous</a> </ul> <li><a href="#Heading17">Global Objects: Construction and Destruction</a> <li><a href="#Heading18">Advanced Exception Handling Techniques</a> <ul> <li><a href="#Heading19">Standard Exceptions</a> <li><a href="#Heading20">Exception Handlers Hierarchy</a> <li><a href="#Heading21">Rethrowing an Exception</a> <li><a href="#Heading22">Function try Blocks</a> <li><a href="#Heading23">Use auto_ptr<> to Avoid Memory Leaks</a> </ul> <li><a href="#Heading24"> Exception Handling Performance Overhead</a> <ul> <li><a href="#Heading25">Additional Runtime Type Information</a> <li><a href="#Heading26">Toggling Exception Handling Support</a> </ul> <li><a href="#Heading27">Misuses of Exception Handling</a> <li><a href="#Heading28">Conclusions</a> </ul><hr size=4><h2> <a name="Heading1">Introduction</a></h2><p>Large software applications are built in layers. At the lowest level, you usually find library routines, API functions, and proprietary infrastructure functions. At the highest level, there are user interface components that enable a user to, for instance, fill out a data sheet in a spreadsheet application. Consider an ordinary flight-booking application: its topmost layer consists of GUI components that display contents on the user's screen. These high-level components interact with data access objects, which in turn encapsulate database API routines. At a lower level, the database API routines interact with the database engine. The database engine itself invokes system services that deal with low-level hardware resources such as physical memory, file system, and security modules. In general, severe runtime errors are detected in these lower code layers, which cannot -- or should not -- attempt to handle these errors on their own. The handling of severe runtime errors is the responsibility of higher-level components. In order to handle an error, however, higher-level components have to be informed that an error has occurred. Essentially, error handling consists of detecting an error and notifying the software components that are in charge. These components in turn handle the error and attempt to recover from it.</p><h2> <a name="Heading2">Traditional Error Handling Methods</a></h2><p>In its earlier stages, C++ did not have a built-in facility for handling runtime errors. Instead, the traditional C methods were used for that purpose. These methods can be grouped into three design policies:</p><ul> <li> <p> Return a status code with agreed-upon values to indicate either success or failure.</p> </li> <p></p> <li> <p> Assign an error code to a global variable and have other functions examine it.</p> </li> <p></p> <li> <p> Terminate the program altogether.</p> </li></ul><p></p><p>Each of these methods has significant drawbacks and limitations in an object-oriented environment. Some of them might be totally unacceptable, particularly in large-scale applications. The following sections examine each of these methods more closely in order to assess their inherent limitations and hazards.</p><h3> <a name="Heading3">Returning an Error Code</a></h3><p>To some extent, this method can be useful in small programs in which an agreed-upon, closed set of error codes exists, and in which a rigorous policy of reporting errors and checking the returned status of a function is applied. However, this method has some noticeable limitations; for example, neither the error types nor their enumerated values are standardized. Thus, in one library the implementer might choose a return value of <tt>0</tt> (meaning <i>false</i>, perhaps) to indicate an error, whereas another vendor might choose <tt>0</tt> to indicate success and any nonzero value to indicate an error condition. Usually, the return codes are shared in a common header file in the form of symbolic constants so that some commonality can be maintained throughout an application or a development team. These codes are not standardized, however.</p><p>Needless to say, the process of combining noncompatible software libraries from different vendors or programmers becomes very difficult and confusing when conflicting error codes are used. Another disadvantage is that every returned code has to be looked up and interpreted -- a tedious and costly operation. This policy requires that the return value of every function be checked every time by every caller; failing to do so might lead to runtime disasters. When an error code is detected, a return statement disrupts the normal execution flow and passes the error code on to the caller. The additional code that wraps every function call (to examine the return status and decide whether to continue normally) can easily double the size of the program and cause serious difficulties in the software's maintenance and readability. Worse yet, returning an error value is sometimes impossible. For instance, constructors do not return values, so they cannot use this method to report the failed construction of an object. </p><h3> <a name="Heading4">Turning on a Global Flag</a></h3><p>An alternative approach for reporting runtime errors is to use global flags, which indicate whether the last operation ended successfully. Unlike the return code policy, this method is standardized. The C <errno.h> header file defines a mechanism for examining and assigning the value of a global integer flag, <tt>errno</tt>. Note that the inherent drawbacks of this policy are not negligible. In a multithreaded environment, the error code that is assigned to <tt>errno</tt> by one thread can be inadvertently overwritten by another thread before the caller has had a chance to examine it. In addition, the use of an error code instead of a more readable message is disadvantageous because the codes might not be compatible among different environments. Finally, this method requires a well-disciplined programming style that relies on constant checking of the current value of <tt>errno</tt>. </p><p>The global flag policy is similar to the function return value policy: Both provide a mechanism for reporting an error, but neither guarantees that the error is actually handled. For example, a function that fails to open a file can indicate a failure by assigning an appropriate value to <tt>errno</tt>. However, it cannot prevent another function from attempting to write into the file or close it. Furthermore, if <tt>errno</tt> indicates an error and the programmer detects and handles it as is expected, <tt>errno</tt> still has to be reset explicitly. A programmer might forget to do so, thereby causing other functions, which assume that the error has not been handled, to attempt to rectify the problem -- with unpredictable results.</p><h3> <a name="Heading5">Terminating the Program</a></h3><p>The most drastic method of handling a runtime error is simply to terminate the program immediately when a severe error has been detected. This solution averts some of the drawbacks of the previous two methods; for example, there is no need for repetitive examination of the status that is returned from every function call, nor does the programmer have to assign a global flag, test its value, and clear it in a repetitive and error-prone manner. The standard C library has two functions that terminate a program: <tt>exit()</tt> and <tt>abort()</tt>. <tt>exit()</tt> can be called to indicate successful termination of a program (as the final statement in <tt>main()</tt>), or it can be called in the case of a runtime error. Before returning control to the environment, <tt>exit()</tt> first flushes open streams and closes open files. <tt>abort()</tt>, on the other hand, indicates abnormal program termination. It terminates immediately, without flushing streams or closing open files.</p><p>Critical applications cannot just halt abruptly on every occurrence of a runtime error. It would be disastrous if a life support machine stopped functioning just because its controller detected a division by zero; likewise, the embedded computer that controls the automatic functions of a manned space shuttle should not halt just because it temporarily loses communication with ground control. Similarly, applications such as the billing system of a telephone company or a banking application cannot break down altogether whenever a runtime exception occurs. Robust, real world applications can -- and must -- do better than that.</p><p>Program termination is problematic even for applications, such as an operating system, that are expected to abort in the case of serious runtime errors. A function that detects the error usually does not have the necessary information to estimate the severity of the error. A memory allocation function, for example, cannot tell whether an allocation request has failed because the user is currently using a debugger, a Web browser, a spreadsheet, and a word processor all at once, or because the system has become unstable due to a severe hardware fault. In the first scenario, the system can simply display a message, requesting that the user close unnecessary applications. In the second scenario, a more drastic measure might be required. Under this policy, however, the allocation function simply aborts the program (the operating system kernel, in this case), regardless of the severity of the error. This is hardly applicable in nontrivial applications. Good system design has to ensure that runtime errors are detected and reported, but it also has to ensure a minimal level of fault tolerance.</p><p>Terminating the program might be acceptable under extreme conditions or during debugging phases. However, <tt>abort()</tt> and <tt>exit()</tt> are never to be used in an object-oriented environment, even during debugging, because they are unaware of the C++ object model.</p><h4> exit() and abort() Do Not Destroy Objects</h4><p>An object can hold resources that are acquired by the constructor or a member function: memory allocated on the free store, file handles, communication ports, database transaction locks, I/O devices, and so on. These resources have to be properly released by the object that uses them when it's done. Usually, these resources are released by the destructor. This design idiom is called <i>resource initialization is acquisition</i> (this is discussed in greater detail in Chapter 5, "Object-Oriented Program and Design"). Local objects that are created on the stack are destroyed automatically when their block or function exits. Neither <tt>abort() nor exit()</tt>, however, invokes the destructors of local objects. Therefore, an abrupt program termination caused by calling these functions can cause irreversible damage: A database can be corrupted, files can be lost, and valuable data can evaporate. For this reason, do not use either <tt>abort()</tt> or <tt>exit()</tt> in an object-oriented environment.</p><h2> <a name="Heading6">Enter Exception Handling</a></h2><p>As you have observed, none of the traditional error handling methods of C are adequate for C++; one of the goals of C++ was to enable better and safer large-scale software development than is offered by C.</p><p>The designers of C++ were aware of the considerable difficulties resulting from the lack of a proper error handling mechanism. They sought a solution that was free from all the ailments of C's traditional error handling. The suggested mechanism was based on the automatic transfer of control to the system when an exception is triggered. The mechanism had to be simple, and it had to free the programmer from the drudgery of constantly examining a global flag or the returned value of a function. Additionally, it had to ensure that the code sections that handle the exception are automatically informed when an exception occurs. Finally, it had to ensure that when an exception is not handled locally, local objects are properly destroyed and their resources are released before the exception is propagated to a higher handler.</p><p>In 1989, after several years of research and a plethora of draft proposals, exception handling was added to C++. C++ is not the first language to offer structured runtime error handling support. Back in the 1960s, PL/1 offered a built-in exception handling mechanism; Ada provided its own version of exception handling in the early 1980s, as did several other languages. But none of these exception handling models fit the C++ object model and program structure. Therefore, the proposed exception handling for C++ was unique, and it has served as a model for newer languages that have appeared since. </p><p>Implementing an exception handling mechanism turned out to be a real challenge. The first C++ compiler, cfront, ran on UNIX. Like many UNIX compilers, it was a translator that first transformed C++ code into C, and then compiled the resultant C code. Release 4.0 of cfront was supposed to include exception handling. However, the implementation of an exception handling mechanism that met all the requirements got so complicated that the development team of cfront 4.0 decided to abandon the project entirely after spending a whole year designing it. cfront 4.0 was never released; however, exception handling became an integral part of Standard C++. Other compilers that started to appear later supported it. The following section explains why it was it so difficult to implement exception handling under cfront, and under any other compiler in general.</p>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -