16. Exception Handling

Objectives

In this chapter you’ll learn:

Image  What exceptions are and when to use them.

Image  To use try, catch and throw to detect, handle and indicate exceptions, respectively.

Image  To process uncaught and unexpected exceptions.

Image  To declare new exception classes.

Image  How stack unwinding enables exceptions not caught in one scope to be caught in another scope.

Image  To handle new failures.

Image  To use auto_ptr to prevent memory leaks.

Image  To understand the standard exception hierarchy.

It is common sense to take a method and try it. If it fails, admit it frankly and try another. But above all, try something.

Franklin Delano Roosevelt

O! throw away the worser part of it, And live the purer with the other half.

William Shakespeare

If they’re running and they don’t look where they’re going I have to come out from somewhere and catch them.

Jerome David Salinger

O infinite virtue! com’st thou smiling from the world’s great snare uncaught?

William Shakespeare

I never forget a face, but in your case I’ll make an exception.

Groucho Marx

Outline

16.1 Introduction

In this chapter, we introduce exception handling. An exception is an indication of a problem that occurs during a program’s execution. The name “exception” implies that the problem occurs infrequently—if the “rule” is that a statement normally executes correctly, then the “exception to the rule” is that a problem occurs. Exception handling enables programmers to create applications that can resolve (or handle) exceptions. In many cases, handling an exception allows a program to continue executing as if no problem had been encountered. A more severe problem could prevent a program from continuing normal execution, instead requiring the program to notify the user of the problem before terminating in a controlled manner. The features presented in this chapter enable programmers to write robust and fault-tolerant programs that are able to deal with problems that may arise and continue executing or terminate gracefully. The style and details of C++ exception handling are based in part on the work of Andrew Koenig and Bjarne Stroustrup, as presented in their paper, “Exception Handling for C++ (revised).”1

Error-Prevention Tip 16.1

Error-Prevention Tip 16.1

Exception handling helps improve a program’s fault tolerance.

Software Engineering Observation 16.1

Software Engineering Observation 16.1

Exception handling provides a standard mechanism for processing errors. This is especially important when working on a project with a large team of programmers.

The chapter begins with an overview of exception-handling concepts, then demonstrates basic exception-handling techniques. We show these techniques via an example that demonstrates handling an exception that occurs when a function attempts to divide by zero. We then discuss additional exception-handling issues, such as how to handle exceptions that occur in a constructor or destructor and how to handle exceptions that occur if operator new fails to allocate memory for an object. We conclude the chapter by introducing several classes that the C++ Standard Library provides for handling exceptions.

16.2 Exception-Handling Overview

Program logic frequently tests conditions that determine how program execution proceeds. Consider the following pseudocode:

Perform a task

If the preceding task did not execute correctly
     Perform error processing

Perform next task

If the preceding task did not execute correctly
     Perform error processing
...

In this pseudocode, we begin by performing a task. We then test whether that task executed correctly. If not, we perform error processing. Otherwise, we continue with the next task. Although this form of error handling works, intermixing program logic with error-handling logic can make the program difficult to read, modify, maintain and debug—especially in large applications.

Performance Tip 16.1

Performance Tip 16.1

If the potential problems occur infrequently, intermixing program logic and error-handling logic can degrade a program’s performance, because the program must (potentially frequently) perform tests to determine whether the task executed correctly and the next task can be performed.

Exception handling enables you to remove error-handling code from the “main line” of the program’s execution, which improves program clarity and enhances modifiability. Programmers can decide to handle any exceptions they choose—all exceptions, all exceptions of a certain type or all exceptions of a group of related types (e.g., exception types that belong to an inheritance hierarchy). Such flexibility reduces the likelihood that errors will be overlooked and thereby makes a program more robust.

With programming languages that do not support exception handling, programmers often delay writing error-processing code or sometimes forget to include it. This results in less robust software products. C++ enables you to deal with exception handling easily from the inception of a project.

16.3 Example: Handling an Attempt to Divide by Zero

Let us consider a simple example of exception handling (Figs. 16.116.2). The purpose of this example is to show how to prevent a common arithmetic problem—division by zero. In C++, division by zero using integer arithmetic typically causes a program to terminate prematurely. In floating-point arithmetic, some C++ implementations allow division by zero, in which case positive or negative infinity is displayed as INF or -INF, respectively.

Fig. 16.1 Class DivideByZeroException definition.

 1   // Fig. 16.1: DivideByZeroException.h
 2   // Class DivideByZeroException definition.
 3   #include <stdexcept> // stdexcept header file contains runtime_error 
 4   using std::runtime_error; // standard C++ library class runtime_error
 5
 6   // DivideByZeroException objects should be thrown by functions
 7   // upon detecting division-by-zero exceptions
 8   class DivideByZeroException : public runtime_error
 9   {
10   public:
11      // constructor specifies default error message
12      DivideByZeroException()
13         : runtime_error( "attempted to divide by zero" ) {}
14   }; // end class DivideByZeroException

Fig. 16.2 Exception-handling example that throws exceptions on attempts to divide by zero.

 1   // Fig. 16.2: Fig16_02.cpp
 2   // A simple exception-handling example that checks for
 3   // divide-by-zero exceptions.
 4   #include <iostream>
 5   using std::cin;
 6   using std::cout;
 7   using std::endl;
 8
 9   #include "DivideByZeroException.h" // DivideByZeroException class 
10
11   // perform division and throw DivideByZeroException object if
12   // divide-by-zero exception occurs
13   double quotient( int numerator, int denominator )
14   {
15      // throw DivideByZeroException if trying to divide by zero
16      if ( denominator == 0 )
17         throw DivideByZeroException(); // terminate function
18
19      // return division result
20      return static_cast< double >( numerator ) / denominator;
21   } // end function quotient
22
23   int main()
24   {
25      int number1; // user-specified numerator
26      int number2; // user-specified denominator
27      double result; // result of division
28
29      cout << "Enter two integers (end-of-file to end): ";
30
31      // enable user to enter two integers to divide
32      while ( cin >> number1 >> number2 )
33      {
34         // try block contains code that might throw exception     
35         // and code that should not execute if an exception occurs
36         try                                                       
37         {                                                         
38            result = quotient( number1, number2 );                 
39            cout << "The quotient is: " << result << endl;         
40         // end try                                              
41                                                                   
42         // exception handler handles a divide-by-zero exception   
43         catch ( DivideByZeroException &divideByZeroException )    
44         {                                                         
45            cout << "Exception occurred: "                         
46               << divideByZeroException.what() << endl;            
47         // end catch                                            
48
49         cout << " Enter two integers (end-of-file to end): ";
50      } // end while
51
52      cout << endl;
53      return 0// terminate normally
54   } // end main

Enter two integers (end-of-file to end): 100 7
The quotient is: 14.2857

Enter two integers (end-of-file to end): 100 0
Exception occurred: attempted to divide by zero

Enter two integers (end-of-file to end): ^Z

In this example, we define a function named quotient that receives two integers input by the user and divides its first int parameter by its second int parameter. Before performing the division, the function casts the first int parameter’s value to type double. Then, the second int parameter’s value is promoted to type double for the calculation. So function quotient actually performs the division using two double values and returns a double result.

Although division by zero is allowed in floating-point arithmetic, for the purpose of this example we treat any attempt to divide by zero as an error. Thus, function quotient tests its second parameter to ensure that it is not zero before allowing the division to proceed. If the second parameter is zero, the function uses an exception to indicate to the caller that a problem occurred. The caller (main in this example) can then process the exception and allow the user to type two new values before calling function quotient again. In this way, the program can continue to execute even after an improper value is entered, thus making the program more robust.

The example consists of two files. DivideByZeroException.h (Fig. 16.1) defines an exception class that represents the type of the problem that might occur in the example, and fig16_02.cpp (Fig. 16.2) defines the quotient function and the main function that calls it. Function main contains the code that demonstrates exception handling.

Defining an Exception Class to Represent the Type of Problem That Might Occur

Figure 16.1 defines class DivideByZeroException as a derived class of Standard Library class runtime_error (defined in header file <stdexcept>). Class runtime_error—a derived class of Standard Library class exception (defined in header file <exception>)—is the C++ standard base class for representing runtime errors. Class exception is the standard C++ base class for all exceptions. (Section 16.13 discusses class exception and its derived classes in detail.) A typical exception class that derives from the runtime_error class defines only a constructor (e.g., lines 12–13) that passes an error-message string to the base-class runtime_error constructor. Every exception class that derives directly or indirectly from exception contains the virtual function what, which returns an exception object’s error message. Note that you are not required to derive a custom exception class, such as DivideByZeroException, from the standard exception classes provided by C++. However, doing so allows programmers to use the virtual function what to obtain an appropriate error message. We use an object of this DivideByZeroException class in Fig. 16.2 to indicate when an attempt is made to divide by zero.

Demonstrating Exception Handling

The program in Fig. 16.2 uses exception handling to wrap code that might throw a “divide-by-zero” exception and to handle that exception, should one occur. The application enables the user to enter two integers, which are passed as arguments to function quotient (lines 13–21). This function divides its first parameter (numerator) by its second parameter (denominator). Assuming that the user does not specify 0 as the denominator for the division, function quotient returns the division result. However, if the user inputs 0 for the denominator, function quotient throws an exception. In the sample output, the first two lines show a successful calculation, and the next two lines show a failed calculation due to an attempt to divide by zero. When the exception occurs, the program informs the user of the mistake and prompts the user to input two new integers. After we discuss the code, we’ll consider the user inputs and flow of program control that yield these outputs.

Enclosing Code in a try Block

The program begins by prompting the user to enter two integers. The integers are input in the condition of the while loop (line 32). After the user inputs values that represent the numerator and denominator, program control proceeds into the loop’s body (lines 33–50). Line 38 passes these values to function quotient (lines 13–21), which either divides the integers and returns a result, or throws an exception (i.e., indicates that an error occurred) on an attempt to divide by zero. Exception handling is geared to situations in which the function that detects an error is unable to handle it.

C++ provides try blocks to enable exception handling. A try block consists of keyword try followed by braces ({}) that define a block of code in which exceptions might occur. The try block encloses statements that might cause exceptions and statements that should be skipped if an exception occurs.

Note that a try block (lines 36–40) encloses the invocation of function quotient and the statement that displays the division result. In this example, because the invocation of function quotient (line 38) can throw an exception, we enclose this function invocation in a try block. Enclosing the output statement (line 39) in the try block ensures that the output will occur only if function quotient returns a result.

Software Engineering Observation 16.2

Software Engineering Observation 16.2

Exceptions may surface through explicitly mentioned code in a try block, through calls to other functions and through deeply nested function calls initiated by code in a try block.

Defining a catch Handler to Process a DivideByZeroException

Exceptions are processed by catch handlers (also called exception handlers), which catch and handle exceptions. At least one catch handler (lines 43–47) must immediately follow each try block. Each catch handler begins with the keyword catch and specifies in parentheses an exception parameter that represents the type of exception the catch handler can process (DivideByZeroException in this case). When an exception occurs in a try block, the catch handler that executes is the one whose type matches the type of the exception that occurred (i.e., the type in the catch block matches the thrown exception type exactly or is a base class of it). If an exception parameter includes an optional parameter name, the catch handler can use that parameter name to interact with the caught exception in the body of the catch handler, which is delimited by braces ({ and }). A catch handler typically reports the error to the user, logs it to a file, terminates the program gracefully or tries an alternate strategy to accomplish the failed task. In this example, the catch handler simply reports that the user attempted to divide by zero. Then the program prompts the user to enter two new integer values.

Common Programming Error 16.1

Common Programming Error 16.1

It is a syntax error to place code between a try block and its corresponding catch handlers or between its catch handlers.

Common Programming Error 16.2

Common Programming Error 16.2

Each catch handler can have only a single parameter—specifying a comma-separated list of exception parameters is a syntax error.

Common Programming Error 16.3

Common Programming Error 16.3

It is a logic error to catch the same type in two different catch handlers following a single try block.

Termination Model of Exception Handling

If an exception occurs as the result of a statement in a try block, the try block expires (i.e., terminates immediately). Next, the program searches for the first catch handler that can process the type of exception that occurred. The program locates the matching catch by comparing the thrown exception’s type to each catch’s exception-parameter type until the program finds a match. A match occurs if the types are identical or if the thrown exception’s type is a derived class of the exception-parameter type. When a match occurs, the code contained in the matching catch handler executes. When a catch handler finishes processing by reaching its closing right brace (}), the exception is considered handled and the local variables defined within the catch handler (including the catch parameter) go out of scope. Program control does not return to the point at which the exception occurred (known as the throw point), because the try block has expired. Rather, control resumes with the first statement (line 49) after the last catch handler following the try block. This is known as the termination model of exception handling. [Note: Some languages use the resumption model of exception handling, in which, after an exception is handled, control resumes just after the throw point.] As with any other block of code, when a try block terminates, local variables defined in the block go out of scope.

Common Programming Error 16.4

Common Programming Error 16.4

Logic errors can occur if you assume that after an exception is handled, control will return to the first statement after the throw point.

Error-Prevention Tip 16.2

Error-Prevention Tip 16.2

With exception handling, a program can continue executing (rather than terminating) after dealing with a problem. This helps ensure the kind of robust applications that contribute to what is called mission-critical computing or business-critical computing.

If the try block completes its execution successfully (i.e., no exceptions occur in the try block), then the program ignores the catch handlers and program control continues with the first statement after the last catch following that try block. If no exceptions occur in a try block, the program ignores the catch handler(s) for that block.

If an exception that occurs in a try block has no matching catch handler, or if an exception occurs in a statement that is not in a try block, the function that contains the statement terminates immediately, and the program attempts to locate an enclosing try block in the calling function. This process is called stack unwinding and is discussed in Section 16.8.

Flow of Program Control When the User Enters a Nonzero Denominator

Consider the flow of control when the user inputs the numerator 100 and the denominator 7 (i.e., the first two lines of output in Fig. 16.2). In line 16, function quotient determines that the denominator does not equal zero, so line 20 performs the division and returns the result (14.2857) to line 38 as a double (the static_cast< double > in line 20 ensures the proper return value type). Program control then continues sequentially from line 38, so line 39 displays the division result—line 40 ends the try block. Because the try block completed successfully and did not throw an exception, the program does not execute the statements contained in the catch handler (lines 43–47), and control continues to line 49 (the first line of code after the catch handler), which prompts the user to enter two more integers.

Flow of Program Control When the User Enters a Denominator of Zero

Now let us consider a more interesting case in which the user inputs the numerator 100 and the denominator 0 (i.e., the third and fourth lines of output in Fig. 16.2). In line 16, quotient determines that the denominator equals zero, which indicates an attempt to divide by zero. Line 17 throws an exception, which we represent as an object of class DivideByZeroException (Fig. 16.1).

To throw an exception, line 17 uses keyword throw followed by an operand that represents the type of exception to throw. Normally, a throw statement specifies one operand. (In Section 16.5, we discuss how to use a throw statement with no operand.) The operand of a throw can be of any type. If the operand is an object, we call it an exception object—in this example, the exception object is an object of type DivideByZeroException. However, a throw operand also can assume other values, such as the value of an expression that does not result in an object (e.g., throw x > 5) or the value of an int (e.g., throw 5). The examples in this chapter focus exclusively on throwing exception objects.

Common Programming Error 16.5

Common Programming Error 16.5

Use caution when throwing the result of a conditional expression (?:)—promotion rules could cause the value to be of a type different from the one expected. For example, when throwing an int or a double from the same conditional expression, the int is promoted to a double. So, a catch handler that catches an int would never execute based on such a conditional expression.

As part of throwing an exception, the throw operand is created and used to initialize the parameter in the catch handler, which we discuss momentarily. In this example, the throw statement in line 17 creates an object of class DivideByZeroException. When line 17 throws the exception, function quotient exits immediately. Therefore, line 17 throws the exception before function quotient can perform the division in line 20. This is a central characteristic of exception handling: A function should throw an exception before the error has an opportunity to occur.

Because we decided to enclose the invocation of function quotient (line 38) in a try block, program control enters the catch handler (lines 43–47) that immediately follows the try block. This catch handler serves as the exception handler for the divide-by-zero exception. In general, when an exception is thrown within a try block, the exception is caught by a catch handler that specifies the type matching the thrown exception. In this program, the catch handler specifies that it catches DivideByZeroException objects—this type matches the object type thrown in function quotient. Actually, the catch handler catches a reference to the DivideByZeroException object created by function quotient’s throw statement (line 17). The exception object is maintained by the exception-handling mechanism.

Performance Tip 16.2

Performance Tip 16.2

Catching an exception object by reference eliminates the overhead of copying the object that represents the thrown exception.

Good Programming Practice 16.1

Good Programming Practice 16.1

Associating each type of runtime error with an appropriately named exception object improves program clarity.

The catch handler’s body (lines 45–46) prints the associated error message returned by calling function what of base-class runtime_error. This function returns the string that the DivideByZeroException constructor (lines 12–13 in Fig. 16.1) passed to the runtime_error base-class constructor.

16.4 When to Use Exception Handling

Exception handling is designed to process synchronous errors, which occur when a statement executes. Common examples of these errors are out-of-range array subscripts, arithmetic overflow (i.e., a value outside the representable range of values), division by zero, invalid function parameters and unsuccessful memory allocation (due to lack of memory). Exception handling is not designed to process errors associated with asynchronous events (e.g., disk I/O completions, network message arrivals, mouse clicks and keystrokes), which occur in parallel with, and independent of, the program’s flow of control.

Software Engineering Observation 16.3

Software Engineering Observation 16.3

Incorporate your exception-handling strategy into your system from the design process’s inception. Including effective exception handling after a system has been implemented can be difficult.

Software Engineering Observation 16.4

Software Engineering Observation 16.4

Exception handling provides a single, uniform technique for processing problems. This helps programmers working on large projects understand each other’s error-processing code.

Software Engineering Observation 16.5

Software Engineering Observation 16.5

Avoid using exception handling as an alternate form of flow of control. These “additional” exceptions can “get in the way” of genuine error-type exceptions.

Software Engineering Observation 16.6

Software Engineering Observation 16.6

Exception handling simplifies combining software components and enables them to work together effectively by enabling predefined components to communicate problems to application-specific components, which can then process the problems in an application-specific manner.

The exception-handling mechanism also is useful for processing problems that occur when a program interacts with software elements, such as member functions, constructors, destructors and classes. Rather than handling problems internally, such software elements often use exceptions to notify programs when problems occur. This enables programmers to implement customized error handling for each application.

Performance Tip 16.3

Performance Tip 16.3

When no exceptions occur, exception-handling code incurs little or no performance penalty. Thus, programs that implement exception handling operate more efficiently than do programs that intermix error-handling code with program logic.

Software Engineering Observation 16.7

Software Engineering Observation 16.7

Functions with common error conditions should return 0 or NULL (or other appropriate values) rather than throw exceptions. A program calling such a function can check the return value to determine success or failure of the function call.

Complex applications normally consist of predefined software components and application-specific components that use the predefined components. When a predefined component encounters a problem, that component needs a mechanism to communicate the problem to the application-specific component—the predefined component cannot know in advance how each application processes a problem that occurs.

16.5 Rethrowing an Exception

It is possible that an exception handler, upon receiving an exception, might decide either that it cannot process that exception or that it can process the exception only partially. In such cases, the exception handler can defer the exception handling (or perhaps a portion of it) to another exception handler. In either case, you achieve this by rethrowing the exception via the statement

throw;

Regardless of whether a handler can process (even partially) an exception, the handler can rethrow the exception for further processing outside the handler. The next enclosing try block detects the rethrown exception, which a catch handler listed after that enclosing try block attempts to handle.

Common Programming Error 16.6

Common Programming Error 16.6

Executing an empty throw statement that is situated outside a catch handler causes a call to function terminate, which abandons exception processing and terminates the program immediately.

The program of Fig. 16.3 demonstrates rethrowing an exception. In main’s try block (lines 32–37), line 35 calls function throwException (lines 11–27). The throwException function also contains a try block (lines 14–18), from which the throw statement in line 17 throws an instance of standard-library-class exception. Function throwException’s catch handler (lines 19–24) catches this exception, prints an error message (lines 21–22) and rethrows the exception (line 23). This terminates function throwException and returns control to line 35 in the try...catch block in main. The try block terminates (so line 36 does not execute), and the catch handler in main (lines 38–41) catches this exception and prints an error message (line 40). [Note: Since we do not use the exception parameters in the catch handlers of this example, we omit the exception parameter names and specify only the type of exception to catch (lines 19 and 38).]

Fig. 16.3 Rethrowing an exception.

 1   // Fig. 16.3: Fig16_03.cpp
 2   // Demonstrating exception rethrowing.
 3   #include <iostream>
 4   using std::cout;
 5   using std::endl;
 6
 7   #include <exception>
 8   using std::exception;
 9
10   // throw, catch and rethrow exception
11   void throwException()
12   {
13      // throw exception and catch it immediately
14      try
15      {
16         cout << "  Function throwException throws an exception ";
17         throw exception(); // generate exception
18      } // end try
19      catch ( exception & ) // handle exception
20      {
21         cout << "  Exception handled in function throwException"
22            << "   Function throwException rethrows exception";

23         throw; // rethrow exception for further processing
24      } // end catch
25
26      cout << "This also should not print ";
27   } // end function throwException
28
29   int main()
30   {
31      // throw exception
32      try
33      {
34         cout << " main invokes function throwException ";
35         throwException();
36         cout << "This should not print ";
37      } // end try
38      catch ( exception & ) // handle exception
39      {
40         cout << " Exception handled in main ";
41      } // end catch
42
43      cout << "Program control continues after catch in main ";
44      return 0;
45   } // end main

main invokes function throwException
  Function throwException throws an exception
  Exception handled in function throwException
  Function throwException rethrows exception

Exception handled in main
Program control continues after catch in main

16.6 Exception Specifications

An optional exception specification (also called a throw list) enumerates a list of exceptions that a function can throw. For example, consider the function declaration

int someFunction( double value )
   throw ( ExceptionA, ExceptionB, ExceptionC )
{
   // function body
}

In this definition, the exception specification, which begins with keyword throw immediately following the closing parenthesis of the function’s parameter list, indicates that function someFunction can throw exceptions of types ExceptionA, ExceptionB and ExceptionC. A function can throw only exceptions of the types indicated by the specification or exceptions of any type derived from these types. If the function throws an exception that does not belong to a specified type, the exception-handling mechanism calls function unexpected, which terminates the program.

A function that does not provide an exception specification can throw any exception. Placing throw()—an empty exception specification—after a function’s parameter list states that the function does not throw exceptions. If the function attempts to throw an exception, function unexpected is invoked. Section 16.7 shows how function unexpected can be customized by calling function set_unexpected.

Common Programming Error 16.7

Common Programming Error 16.7

Throwing an exception that has not been declared in a function’s exception specification causes a call to function unexpected.

Error-Prevention Tip 16.3

Error-Prevention Tip 16.3

The compiler will not generate a compilation error if a function contains a throw expression for an exception not listed in the function’s exception specification. An error occurs only when that function attempts to throw that exception at execution time. To avoid surprises at execution time, carefully check your code to ensure that functions do not throw exceptions not listed in their exception specifications.

16.7 Processing Unexpected Exceptions

Function unexpected calls the function registered with function set_unexpected (defined in header file <exception>). If no function has been registered in this manner, function terminate is called by default. Cases in which function terminate is called include:

1.   the exception mechanism cannot find a matching catch for a thrown exception

2.   a destructor attempts to throw an exception during stack unwinding

3.   an attempt is made to rethrow an exception when there is no exception currently being handled

4.   a call to function unexpected defaults to calling function terminate

(Section 15.5.1 of the C++ Standard Document discusses several additional cases.) Function set_terminate can specify the function to invoke when terminate is called. Otherwise, terminate calls abort, which terminates the program without calling the destructors of any remaining objects of automatic or static storage class. This could lead to resource leaks when a program terminates prematurely.

Function set_terminate and function set_unexpected each return a pointer to the last function called by terminate and unexpected, respectively (0, the first time each is called). This enables you to save the function pointer so it can be restored later. Functions set_terminate and set_unexpected take as arguments pointers to functions with void return types and no arguments.

If the last action of a programmer-defined termination function is not to exit a program, function abort will be called to end program execution after the other statements of the programmer-defined termination function are executed.

16.8 Stack Unwinding

When an exception is thrown but not caught in a particular scope, the function call stack is “unwound,” and an attempt is made to catch the exception in the next outer try...catch block. Unwinding the function call stack means that the function in which the exception was not caught terminates, all local variables in that function are destroyed and control returns to the statement that originally invoked that function. If a try block encloses that statement, an attempt is made to catch the exception. If a try block does not enclose that statement, stack unwinding occurs again. If no catch handler ever catches this exception, function terminate is called to terminate the program. The program of Fig. 16.4 demonstrates stack unwinding.

Fig. 16.4 Stack unwinding.

 1   // Fig. 16.4: Fig16_04.cpp
 2   // Demonstrating stack unwinding.
 3   #include <iostream>
 4   using std::cout;
 5   using std::endl;
 6
 7   #include <stdexcept>
 8   using std::runtime_error;
 9
10   // function3 throws runtime error
11   void function3() throw ( runtime_error )
12   {
13      cout << "In function 3" << endl;
14
15      // no try block, stack unwinding occurs, return control to function2
16      throw runtime_error( "runtime_error in function3" ); // no print
17   } // end function3
18
19   // function2 invokes function3
20   void function2() throw ( runtime_error )
21   {
22      cout << "function3 is called inside function2" << endl;
23      function3(); // stack unwinding occurs, return control to function1
24   } // end function2
25
26   // function1 invokes function2
27   void function1() throw ( runtime_error )
28   {
29      cout << "function2 is called inside function1" << endl;
30      function2(); // stack unwinding occurs, return control to main
31   } // end function1
32
33   // demonstrate stack unwinding
34   int main()
35   {
36      // invoke function1
37      try
38      {
39         cout << "function1 is called inside main" << endl;
40         function1(); // call function1 which throws runtime_error
41      } // end try
42      catch ( runtime_error &error ) // handle runtime error
43      {
44         cout << "Exception occurred: " << error.what() << endl;

45         cout << "Exception handled in main" << endl;
46      } // end catch
47
48      return 0;
49   } // end main

function1 is called inside main
function2 is called inside function1
function3 is called inside function2
In function 3
Exception occurred: runtime_error in function3
Exception handled in main

In main, the try block (lines 37–41) calls function1 (lines 27–31). Next, function1 calls function2 (lines 20–24), which in turn calls function3 (lines 11–17). Line 16 of function3 throws a runtime_error object. However, because no try block encloses the throw statement in line 16, stack unwinding occurs—function3 terminates in line 16, then returns control to the statement in function2 that invoked function3 (i.e., line 23). Because no try block encloses line 23, stack unwinding occurs again—function2 terminates in line 23 and returns control to the statement in function1 that invoked function2 (i.e., line 30). Because no try block encloses line 30, stack unwinding occurs one more time—function1 terminates in line 30 and returns control to the statement in main that invoked function1 (i.e., line 40). The try block of lines 37–41 encloses this statement, so the first matching catch handler located after this try block (line 42–46) catches and processes the exception. Line 44 uses function what to display the exception message. Recall that function what is a virtual function of class exception that can be overridden by a derived class to return an appropriate error message.

16.9 Constructors, Destructors and Exception Handling

First, let’s discuss an issue that we have mentioned but not yet resolved satisfactorily: What happens when an error is detected in a constructor? For example, how should an object’s constructor respond when new fails because it was unable to allocate required memory for storing that object’s internal representation? Because the constructor cannot return a value to indicate an error, we must choose an alternative means of indicating that the object has not been constructed properly. One scheme is to return the improperly constructed object and hope that anyone using it would make appropriate tests to determine that it is in an inconsistent state. Another scheme is to set some variable outside the constructor. The preferred alternative is to require the constructor to throw an exception that contains the error information, thus offering an opportunity for the program to handle the failure.

Before an exception is thrown by a constructor, destructors are called for any member objects built as part of the object being constructed. Destructors are called for every automatic object constructed in a try block before an exception is thrown. Stack unwinding is guaranteed to have been completed at the point that an exception handler begins executing. If a destructor invoked as a result of stack unwinding throws an exception, terminate is called.

If an object has member objects, and if an exception is thrown before the outer object is fully constructed, then destructors will be executed for the member objects that have been constructed prior to the occurrence of the exception. If an array of objects has been partially constructed when an exception occurs, only the destructors for the constructed objects in the array will be called.

An exception could preclude the operation of code that would normally release a resource, thus causing a resource leak. One technique to resolve this problem is to initialize a local object to acquire the resource. When an exception occurs, the destructor for that object will be invoked and can free the resource.

Error-Prevention Tip 16.4

Error-Prevention Tip 16.4

When an exception is thrown from the constructor for an object that is created in a new expression, the dynamically allocated memory for that object is released.

16.10 Exceptions and Inheritance

Various exception classes can be derived from a common base class, as we discussed in Section 16.3, when we created class DivideByZeroException as a derived class of class exception. If a catch handler catches a pointer or reference to an exception object of a base-class type, it also can catch a pointer or reference to all objects of classes publicly derived from that base class—this allows for polymorphic processing of related errors.

Error-Prevention Tip 16.5

Error-Prevention Tip 16.5

Using inheritance with exceptions enables an exception handler to catch related errors with concise notation. One approach is to catch each type of pointer or reference to a derived-class exception object individually, but a more concise approach is to catch pointers or references to base-class exception objects instead. Also, catching pointers or references to derived-class exception objects individually is error prone, especially if you forget to test explicitly for one or more of the derived-class pointer or reference types.

16.11 Processing new Failures

The C++ standard specifies that, when operator new fails, it throws a bad_alloc exception (defined in header file <new>). However, some compilers are not compliant with the C++ standard and therefore use the version of new that returns 0 on failure. For example, the Microsoft Visual C++ 2005 throws a bad_alloc exception when new fails, while the Microsoft Visual C++ 6.0 returns 0 on new failure.

Compilers vary in their support for new-failure handling. Many older C++ compilers return 0 by default when new fails. Some compilers support new throwing an exception if header file <new> (or <new.h>) is included. Other compilers throw bad_alloc by default, regardless of whether header file <new> is included. Consult the compiler documentation to determine the compiler’s support for new-failure handling.

In this section, we present three examples of new failing. The first example returns 0 when new fails. The second example uses the version of new that throws a bad_alloc exception when new fails. The third example uses function set_new_handler to handle new failures. [Note: The examples in Figs. 16.516.7 allocate large amounts of dynamic memory, which could cause your computer to become sluggish.]

new Returning 0 on Failure

Figure 16.5 demonstrates new returning 0 on failure to allocate the requested amount of memory. The for statement in lines 13–24 should loop 50 times and, on each pass, allocate an array of 50,000,000 double values (i.e., 400,000,000 bytes, because a double is normally 8 bytes). The if statement in line 17 tests the result of each new operation to determine whether new allocated the memory successfully. If new fails and returns 0, line 19 prints an error message, and the loop terminates. [Note: We used Microsoft Visual C++ 6.0 to run this example, because Microsoft Visual C++ 2005 throws a bad_alloc exception on new failure instead of returning 0.]

Fig. 16.5 Demonstrating pre-standard new returning 0 when memory allocation fails.

 1   // Fig. 16.5: Fig16_05.cpp
 2   // Demonstrating pre-standard new returning 0 when memory
 3   // allocation fails.
 4   #include <iostream>
 5   using std::cerr;
 6   using std::cout;
 7
 8   int main()
 9   {
10      double *ptr[ 50 ];
11
12      // aim each ptr[i] at a big block of memory
13      for ( int i = 0; i < 50; i++ )
14      {
15         ptr[ i ] = new double50000000 ]; // allocate big block
16
17         if ( ptr[ i ] == 0 ) // did new fail to allocate memory
18         {
19            cerr << "Memory allocation failed for ptr[" << i << "] ";
20            break ;
21         } // end if
22         else // successful memory allocation
23            cout << "ptr[" << i << "] points to 50,000,000 new doubles ";
24      } // end for
25
26      return 0;
27   } // end main

ptr[0] points to 50,000,000 new doubles
ptr[1] points to 50,000,000 new doubles
ptr[2] points to 50,000,000 new doubles
Memory allocation failed for ptr[3]

The output shows that the program performed only three iterations before new failed, and the loop terminated. Your output might differ based on the physical memory, disk space available for virtual memory on your system and the compiler you are using.

new Throwing bad_alloc on Failure

Figure 16.6 demonstrates new throwing bad_alloc on failure to allocate the requested memory. The for statement (lines 20–24) inside the try block should loop 50 times and, on each pass, allocate an array of 50,000,000 double values. If new fails and throws a bad_alloc exception, the loop terminates, and the program continues in line 28, where the catch handler catches and processes the exception. Lines 30–31 print the message "Exception occurred:" followed by the message returned from the base-class-exception version of function what (i.e., an implementation-defined exception-specific message, such as "Allocation Failure" in Microsoft Visual C++ 2005). The output shows that the program performed only three iterations of the loop before new failed and threw the bad_alloc exception. Your output might differ based on the physical memory, disk space available for virtual memory on your system and the compiler you are using.

Fig. 16.6 new throwing bad_alloc on failure.

 1   // Fig. 16.6: Fig16_06.cpp
 2   // Demonstrating standard new throwing bad_alloc when memory
 3   // cannot be allocated.
 4   #include <iostream>
 5   using std::cerr;
 6   using std::cout;
 7   using std::endl;
 8
 9   #include <new> // standard operator new
10   using std::bad_alloc;                  
11
12   int main()
13   {
14      double *ptr[ 50 ];
15
16      // aim each ptr[i] at a big block of memory
17      try
18      {
19         // allocate memory for ptr[ i ]; new throws bad_alloc on failure
20         for ( int i = 0; i < 50; i++ )
21         {
22            ptr[ i ] = new double50000000 ]; // may throw exception
23            cout << "ptr[" << i << "] points to 50,000,000 new doubles ";
24         } // end for
25      } // end try
26
27      // handle bad_alloc exception
28      catch ( bad_alloc &memoryAllocationException )
29      {
30         cerr << "Exception occurred: "
31            << memoryAllocationException.what() << endl;
32      } // end catch
33
34      return 0;
35   } // end main

ptr[0] points to 50,000,000 new doubles
ptr[1] points to 50,000,000 new doubles
ptr[2] points to 50,000,000 new doubles
Exception occurred: bad allocation

The C++ standard specifies that standard-compliant compilers can continue to use a version of new that returns 0 upon failure. For this purpose, header file <new> defines object nothrow (of type nothrow_t), which is used as follows:

double *ptr = new( nothrow ) double50000000 ];

The preceding statement uses the version of new that does not throw bad_alloc exceptions (i.e., nothrow) to allocate an array of 50,000,000 doubles.

Software Engineering Observation 16.8

Software Engineering Observation 16.8

To make programs more robust, use the version of new that throws bad_alloc exceptions on failure.

Handling new Failures Using Function set_new_handler

An additional feature for handling new failures is function set_new_handler (prototyped in standard header file <new>). This function takes as its argument a pointer to a function that takes no arguments and returns void. This pointer points to the function that will be called if new fails. This provides you with a uniform approach to handling all new failures, regardless of where a failure occurs in the program. Once set_new_handler registers a new handler in the program, operator new does not throw bad_alloc on failure; rather, it defers the error handling to the new-handler function.

If new allocates memory successfully, it returns a pointer to that memory. If new fails to allocate memory and set_new_handler did not register a new-handler function, new throws a bad_alloc exception. If new fails to allocate memory and a new-handler function has been registered, the new-handler function is called. The C++ standard specifies that the new-handler function should perform one of the following tasks:

1.   Make more memory available by deleting other dynamically allocated memory (or telling the user to close other applications) and return to operator new to attempt to allocate memory again.

2.   Throw an exception of type bad_alloc.

3.   Call function abort or exit (both found in header file <cstdlib>) to terminate the program.

Figure 16.7 demonstrates set_new_handler. Function customNewHandler (lines 14–18) prints an error message (line 16), then terminates the program via a call to abort (line 17). The output shows that the program performed only three iterations of the loop before new failed and invoked function customNewHandler. Your output might differ based on the physical memory, disk space available for virtual memory on your system and the compiler you use to compile the program.

Fig. 16.7 set_new_handler specifying the function to call when new fails.

 1   // Fig. 16.7: Fig16_07.cpp
 2   // Demonstrating set_new_handler.
 3   #include <iostream>
 4   using std::cerr;
 5   using std::cout;
 6

7   #include <new> // standard operator new and set_new_handler
 8   using std::set_new_handler;                                
 9
10   #include <cstdlib> // abort function prototype
11   using std::abort;
12
13   // handle memory allocation failure      
14   void customNewHandler()                  
15   {                                        
16      cerr << "customNewHandler was called";
17      abort();                              
18   // end function customNewHandler       
19
20   // using set_new_handler to handle failed memory allocation
21   int main()
22   {
23      double *ptr[ 50 ];
24
25      // specify that customNewHandler should be called on 
26      // memory allocation failure                         
27      set_new_handler( customNewHandler );                 
28
29      // aim each ptr[i] at a big block of memory; customNewHandler will be
30      // called on failed memory allocation
31      for ( int i = 0; i < 50; i++ )
32      {
33         ptr[ i ] = new double50000000 ]; // may throw exception
34         cout << "ptr[" << i << "] points to 50,000,000 new doubles ";
35      } // end for
36
37      return 0;
38   }  // end main

ptr[0] points to 50,000,000 new doubles
ptr[1] points to 50,000,000 new doubles
ptr[2] points to 50,000,000 new doubles
customNewHandler was called

16.12 Class auto_ptr and Dynamic Memory Allocation

A common programming practice is to allocate dynamic memory, assign the address of that memory to a pointer, use the pointer to manipulate the memory and deallocate the memory with delete when the memory is no longer needed. If an exception occurs after successful memory allocation but before the delete statement executes, a memory leak could occur. The C++ standard provides class template auto_ptr in header file <memory> to deal with this situation.

An object of class auto_ptr maintains a pointer to dynamically allocated memory. When an auto_ptr object destructor is called (for example, when an auto_ptr object goes out of scope), it performs a delete operation on its pointer data member. Class template auto_ptr provides overloaded operators * and -> so that an auto_ptr object can be used just as a regular pointer variable is. Figure 16.10 demonstrates an auto_ptr object that points to a dynamically allocated object of class Integer (Figs. 16.816.9).

Fig. 16.8 Integer class definition.

 1   // Fig. 16.8: Integer.h
 2   // Integer class definition.
 3
 4   class Integer
 5   {
 6   public:
 7      Integer( int i = 0 ); // Integer default constructor
 8      ~Integer(); // Integer destructor
 9      void setInteger( int i ); // functions to set Integer
10      int getInteger() const; // function to return Integer
11   private:
12      int value;
13   }; // end class Integer

Fig. 16.9 Member function definitions of class Integer.

 1   // Fig. 16.9: Integer.cpp
 2   // Integer member function definitions.
 3   #include <iostream>
 4   using std::cout;
 5   using std::endl;
 6
 7   #include "Integer.h"
 8
 9   // Integer default constructor
10   Integer::Integer( int i )
11      : value( i )
12   {
13      cout << "Constructor for Integer " << value << endl;
14   } // end Integer constructor
15
16   // Integer destructor
17   Integer::~Integer()
18   {
19      cout << "Destructor for Integer " << value << endl;
20   } // end Integer destructor
21
22   // set Integer value
23   void Integer::setInteger( int i )
24   {
25      value = i;
26   } // end function setInteger
27
28   // return Integer value
29   int Integer::getInteger() const
30   {
31      return  value;
32   } // end function getInteger

Fig. 16.10 auto_ptr object manages dynamically allocated memory.

 1   // Fig. 16.10: Fig16_10.cpp
 2   // Demonstrating auto_ptr.
 3   #include <iostream>
 4   using std::cout;
 5   using std::endl;
 6
 7   #include <memory>
 8   using std::auto_ptr; // auto_ptr class definition
 9
10   #include "Integer.h"
11
12   // use auto_ptr to manipulate Integer object
13   int main()
14   {
15      cout << "Creating an auto_ptr object that points to an Integer ";
16
17      // "aim" auto_ptr at Integer object                  
18      auto_ptr< Integer > ptrToInteger( new Integer( 7 ) );
19
20      cout << " Using the auto_ptr to manipulate the Integer ";
21      ptrToInteger->setInteger( 99 ); // use auto_ptr to set Integer value
22
23      // use auto_ptr to get Integer value
24      cout << "Integer after setInteger: " << ( *ptrToInteger ).getInteger()
25      return 0;
26   } // end main

Creating an auto_ptr object that points to an Integer
Constructor for Integer 7

Using the auto_ptr to manipulate the Integer
Integer after setInteger: 99

Destructor for Integer 99

Line 18 of Fig. 16.10 creates auto_ptr object ptrToInteger and initializes it with a pointer to a dynamically allocated Integer object that contains the value 7. Line 21 uses the auto_ptr overloaded -> operator to invoke function setInteger on the Integer object that ptrToInteger manages. Line 24 uses the auto_ptr overloaded * operator to dereference ptrToInteger, then uses the dot (.) operator to invoke function getInteger on the Integer object. Like a regular pointer, an auto_ptr’s -> and * overloaded operators can be used to access the object to which the auto_ptr points.

Because ptrToInteger is a local automatic variable in main, ptrToInteger is destroyed when main terminates. The auto_ptr destructor forces a delete of the Integer object pointed to by ptrToInteger, which in turn calls the Integer class destructor. The memory that Integer occupies is released, regardless of how control leaves the block (e.g., by a return statement or by an exception). Most importantly, using this technique can prevent memory leaks. For example, suppose a function returns a pointer aimed at some object. Unfortunately, the function caller that receives this pointer might not delete the object, thus resulting in a memory leak. However, if the function returns an auto_ptr to the object, the object will be deleted automatically when the auto_ptr object’s destructor gets called.

Only one auto_ptr at a time can own a dynamically allocated object and the object cannot be an array. By using its overloaded assignment operator or copy constructor, an auto_ptr can transfer ownership of the dynamic memory it manages. The last auto_ptr object that maintains the pointer to the dynamic memory will delete the memory. This makes auto_ptr an ideal mechanism for returning dynamically allocated memory to client code. When the auto_ptr goes out of scope in the client code, the auto_ptr’s destructor deletes the dynamic memory.

16.13 Standard Library Exception Hierarchy

Experience has shown that exceptions fall nicely into a number of categories. The C++ Standard Library includes a hierarchy of exception classes (Fig. 16.11). As we first discussed in Section 16.3, this hierarchy is headed by base-class exception (defined in header file <exception>), which contains virtual function what, which derived classes can override to issue appropriate error messages.

Fig. 16.11 Standard Library exception classes.

Standard Library exception classes.

Immediate derived classes of base-class exception include runtime_error and logic_error (both defined in header <stdexcept>), each of which has several derived classes. Also derived from exception are the exceptions thrown by C++ operators—for example, bad_alloc is thrown by new (Section 16.11), bad_cast is thrown by dynamic_cast (Chapter 13) and bad_typeid is thrown by typeid (Chapter 13). Including bad_exception in the throw list of a function means that, if an unexpected exception occurs, function unexpected can throw bad_exception rather than terminating the program’s execution (by default) or calling another function specified by set_unexpected.

Common Programming Error 16.8

Common Programming Error 16.8

Placing a catch handler that catches a base-class object before a catch that catches an object of a class derived from that base class is a logic error. The base-class catch catches all objects of classes derived from that base class, so the derived-class catch will never execute.

Class logic_error is the base class of several standard exception classes that indicate errors in program logic. For example, class invalid_argument indicates that an invalid argument was passed to a function. (Proper coding can, of course, prevent invalid arguments from reaching a function.) Class length_error indicates that a length larger than the maximum size allowed for the object being manipulated was used for that object. Class out_of_range indicates that a value, such as a subscript into an array, exceeded its allowed range of values.

Class runtime_error, which we used briefly in Section 16.8, is the base class of several other standard exception classes that indicate execution-time errors. For example, class overflow_error describes an arithmetic overflow error (i.e., the result of an arithmetic operation is larger than the largest number that can be stored in the computer) and class underflow_error describes an arithmetic underflow error (i.e., the result of an arithmetic operation is smaller than the smallest number that can be stored in the computer).

Common Programming Error 16.9

Common Programming Error 16.9

Programmer-defined exception classes need not be derived from class exception. Thus, writing catch( exception anyException ) is not guaranteed to catch all exceptions a program could encounter.

Error-Prevention Tip 16.6

Error-Prevention Tip 16.6

To catch all exceptions potentially thrown in a try block, use catch(...). One weakness with catching exceptions in this way is that the type of the caught exception is unknown at compile time. Another weakness is that, without a named parameter, there is no way to refer to the exception object inside the exception handler.

Software Engineering Observation 16.9

Software Engineering Observation 16.9

The standard exception hierarchy is a good starting point for creating exceptions. Programmers can build programs that can throw standard exceptions, throw exceptions derived from the standard exceptions or throw their own exceptions not derived from the standard exceptions.

Software Engineering Observation 16.10

Software Engineering Observation 16.10

Use catch(...) to perform recovery that does not depend on the exception type (e.g., releasing common resources). The exception can be rethrown to alert more specific enclosing catch handlers.

16.14 Other Error-Handling Techniques

We have discussed several ways to deal with exceptional situations prior to this chapter. The following summarizes these and other error-handling techniques:

•     Ignore the exception. If an exception occurs, the program might fail as a result of the uncaught exception. This is devastating for commercial software products and special-purpose mission-critical software, but, for software developed for your own purposes, ignoring many kinds of errors is common.

Common Programming Error 16.10

Common Programming Error 16.10

Aborting a program component due to an uncaught exception could leave a resource—such as a file stream or an I/O device—in a state in which other programs are unable to acquire the resource. This is known as a resource leak.

•     Abort the program. This, of course, prevents a program from running to completion and producing incorrect results. For many types of errors, this is appropriate, especially for nonfatal errors that enable a program to run to completion (potentially misleading you to think that the program functioned correctly). This strategy is inappropriate for mission-critical applications. Resource issues also are important here—if a program obtains a resource, the program should release that resource before program termination.

•     Set error indicators. The problem with this approach is that programs might not check these error indicators at all points at which the errors could be troublesome. Another problem is that the program, after processing the problem, might not clear the error indicators.

•     Test for the error condition, issue an error message and call exit (in <cstdlib>) to pass an appropriate error code to the program’s environment.

•     Use functions setjump and longjump. These <csetjmp> library functions enable you to specify an immediate jump from a deeply nested function call to an error handler. Without using setjump or longjump, a program must execute several returns to exit the deeply nested function calls. Functions setjump and longjump are dangerous, because they unwind the stack without calling destructors for automatic objects. This can lead to serious problems.

•     Certain kinds of errors have dedicated capabilities for handling them. For example, when operator new fails to allocate memory, a new_handler function can be called to handle the error. This function can be customized by supplying a function name as the argument to set_new_handler, as we discuss in Section 16.11.

16.15 Wrap-Up

In this chapter, you learned how to use exception handling to deal with errors in a program. You learned that exception handling enables programmers to remove error-handling code from the “main line” of the program’s execution. We demonstrated exception handling in the context of a divide-by-zero example. We also showed how to use try blocks to enclose code that may throw an exception, and how to use catch handlers to deal with exceptions that may arise. You learned how to throw and rethrow exceptions, and how to handle the exceptions that occur in constructors. The chapter continued with discussions of processing new failures, dynamic memory allocation with class auto_ptr and the standard library exception hierarchy. In the next chapter, you’ll learn about file processing, including how persistent data is stored and how to manipulate it.

1. Koenig, A., and B. Stroustrup, “Exception Handling for C++ (revised),” Proceedings of the Usenix C++ Conference, pp. 149–176, San Francisco, April 1990.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset