Exception Handling in C++
Date: Apr 18, 2003
Sample Chapter is provided courtesy of Addison Wesley.
Exception handling is a mechanism that allows two separately developed program components to communicate when a program anomaly, called an exception, is encountered during the execution of the program. In this chapter we first look at how to raise, or throw, an exception at the location where the program anomaly is encountered. We then look at how to associate handlers, or catch clauses, with a set of program statements using a try block, and we look at how exceptions are handled by catch clauses. We then introduce exception specifications, a mechanism that associates a list of exceptions with a function declaration and that guarantees that the function does not throw any other types of exceptions. We end the chapter with a discussion of design considerations for programs that use exceptions.
11.1 Throwing an Exception
Exceptions are run-time anomalies that a program may detect, such as division by 0, access to an array outside of its bounds, or the exhaustion of the free store memory. Such exceptions exist outside the normal functioning of the program and require immediate handling by the program. The C++ language provides built-in language features to raise and handle exceptions. These language features activate a run-time mechanism used to communicate exceptions between two unrelated (often separately developed) portions of a C++ program.
When an exception is encountered in a C++ program, the portion of the program that detects the exception can communicate that the exception has occurred by raising, or throwing, an exception. To see how exceptions are thrown in C++, let's reimplement the class iStack presented in Section 4.15 to use exceptions to indicate anomalies in the handling of the stack. The definition of the class iStack looks like this:
#include <vector> class iStack { public: iStack( int capacity ) : _stack( capacity ), _top( 0 ) { } bool pop( int &top_value ); bool push( int value ); bool full(); bool empty(); void display(); int size(); private: int _top; vector< int > _stack; };
The stack is implemented using a vector of ints. When an iStack object is created, the constructor for iStack creates a vector of ints of the size specified with the initial value. This size is the maximum number of elements the iStack object can contain. The following, for example, creates an iStack object called myStack that can contain as many as 20 values of type int:
iStack myStack(20);
What can go wrong when we manipulate myStack? Here are two anomalies that may be encountered with our iStack class:
A pop() operation is requested and the stack is empty.
A push() operation is requested and the stack is full.
We decide that these anomalies should be communicated to the functions manipulating iStack objects using exceptions. So where do we start?
First, we must define the exceptions that can be thrown. In C++, exceptions are most often implemented using classes. Although classes are fully introduced in Chapter 13, we will define here two simple classes to use as exceptions with our iStack class, and we place these class definitions in the header stackExcp.h:
// stackExcp.h class popOnEmpty { /* ... */ }; class pushOnFull { /* ... */ };
Chapter 19 discusses exceptions of class type in greater detail and discusses the exception class hierarchy provided by the C++ standard library.
We must then change the definition of the member functions pop() and push() to throw these newly defined exceptions. An exception is thrown using a throw expression. A throw expression looks a great deal like a return statement. A throw expression is composed of the keyword throw followed by an expression whose type is that of the exception thrown. What does the throw expression in pop() look like? Let's try this:
// oops, not quite right throw popOnEmpty;
Unfortunately, this is not quite right. An exception is an object, and pop() must throw an object of class type. The expression in the throw expression cannot simply be a type. To create an object of class type, we need to call the class constructor. What does a throw expression that invokes a constructor look like? Here is the throw expression in pop():
// expression is a constructor call throw popOnEmpty();
This throw expression creates an exception object of type popOnEmpty.
Recall that the member functions pop() and push() were defined to return a value of type bool: a true return value indicates that the operation succeeded, and a false return value indicates that it failed. Because exceptions are now used to indicate the failure of the pop() and push() operations, the return values from these functions are now unnecessary. We now define these member functions with a void return type. For example:
class iStack { public: // ... // no longer return a value void pop( int &value ); void push( int value ); private: // ... };
The functions that use our iStack class will now assume that everything is fine unless an exception is thrown; they no longer need to test the return value of the member function pop() or push() to see whether the operation succeeds. We will see how to define a function to handle exceptions in the next two sections.
We are now ready to provide the new implementations of iStack's pop() and push() member functions:
#include "stackExcp.h" void iStack::pop( int &top_value ) { if ( empty() ) throw popOnEmpty(); top_value = _stack[ --_top ]; cout << "iStack::pop(): " << top_value << endl; } void iStack::push( int value ) { cout << "iStack::push( " << value << " )\n"; if ( full() ) throw pushOnFull(); _stack[ _top++ ] = value; }
Although exceptions are most often objects of class type, a throw expression can throw an object of any type. For example, although it's unusual, the function mathFunc() in the following code sample throws an exception object of enumeration type. This code is valid C++ code:
enum EHstate { noErr, zeroOp, negativeOp, severeError }; int mathFunc( int i ) { if ( i == 0 ) throw zeroOp; // exception of enumeration type // otherwise, normal processing continues }
Exercise 11.1
Which, if any, of the following throw expressions are errors? Why? For the valid throw expressions, indicate the type of the exception thrown.
(a) class exceptionType { }; throw exceptionType(); (b) int excpObj; throw excpObj; (c) enum mathErr { overflow, underflow, zeroDivide }; throw zeroDivide(); (d) int *pi = &excpObj; throw pi;
Exercise 11.2
The IntArray class defined in Section 2.3 has a member operator function operator[]() that uses assert() to indicate that the index is outside the bounds of the array. Change the definition of operator[]() to instead throw an exception in this situation. Define an exception class to be used as the type of the exception thrown.
11.2 The Try Block
The following small program exercises our class iStack and the pop() and push() member functions defined in the previous section. The for loop in main() iterates 50 times. It pushes on the stack each value that is a multiple of 3 — 3, 6, 9, and so on. Whenever the value is a multiple of 4, such as 4, 8, 12, and so on, it displays the contents of the stack. Whenever the value is a multiple of 10, such as 10, 20, 30, and so on, it pops the last item from the stack and then displays the contents of the stack again. How do we change main() to handle the exceptions thrown by the iStack member functions?
#include <iostream> #include "iStack.h" int main() { iStack stack( 32 ); stack.display(); for ( int ix = 1; ix < 51; ++ix ) { if ( ix % 3 == 0 ) stack.push( ix ); if ( ix % 4 == 0 ) stack.display(); if ( ix % 10 == 0) { int dummy; stack.pop( dummy ); stack.display(); } } return 0; }
A try block must enclose the statements that can throw exceptions. A try block begins with the try keyword followed by a sequence of program statements enclosed in braces. Following the try block is a list of handlers called catch clauses. The try block groups a set of statements and associates with these statements a set of handlers to handle the exceptions that the statements can throw. Where should we place a try block or try blocks in the function main() to handle the exceptions popOnEmpty and pushOnFull? Let's try this:
for ( int ix = 1; ix < 51; ++ix ) { try { // try block for pushOnFull exceptions if ( ix % 3 == 0 ) stack.push( ix ); } catch ( pushOnFull ) { ... } if ( ix % 4 == 0 ) stack.display(); try { // try block for popOnEmpty exceptions if ( ix % 10 == 0 ) { int dummy; stack.pop( dummy ); stack.display(); } } catch ( popOnEmpty ) { ... } }
The program as we have implemented it works correctly. Its organization, however, intermixes the handling of the exceptions with the normal processing of the program and thus is not ideal. After all, exceptions are program anomalies that occur only in exceptional cases. We want to separate the code that handles the program anomalies from the code that implements the normal manipulation of the stack. We believe that this strategy makes the code easier to follow and easier to maintain. Here is our preferred solution:
try { for ( int ix = 1; ix < 51; ++ix ) { if ( ix % 3 == 0 ) stack.push( ix ); if ( ix % 4 == 0 ) stack.display(); if ( ix % 10 == 0 ) { int dummy; stack.pop( dummy ); stack.display(); } } } catch ( pushOnFull ) { ... } catch ( popOnEmpty ) { ... }
Associated with the try block are two catch clauses that are capable of handling the exceptions pushOnFull and popOnEmpty that may be thrown from the iStack member functions push() and pop() called from within the try block. Each catch clause specifies within parentheses the type of exception it handles. The code to handle the exception is placed in the compound statement of the catch clause (between the curly braces). We examine catch clauses in greater detail in the next section.
The program control flow in our example is one of the following.
If no exception occurs, the code within the try block is executed and the handlers associated with the try block are ignored. The function main() returns 0.
If the push() member function called within the first if statement of the for loop throws an exception, the second and third if statements of the for loop are ignored, the for loop and the try block are exited, and the handler for exceptions of type pushOnFull is executed.
If the pop() member function called within the third if statement of the for loop throws an exception, the call to display() is ignored, the for loop and the try block are exited, and the handler for exceptions of type popOnEmpty is executed.
When an exception is thrown, the statements following the statement that throws the exception are skipped. Program execution resumes in the catch clause handling the exception. If no catch clause capable of handling the exception exists, program execution resumes in the function terminate() defined in C++ standard library. We further discuss the function terminate() in the next section.
A try block can contain any C++ statement — expressions as well as declarations. A try block introduces a local scope, and variables declared within a try block cannot be referred to outside the try block, including within the catch clauses. For example, we could rewrite our function main() so that the declaration of the variable stack appears within the try block. In this case, it is not possible to refer to stack in the catch clauses:
int main() { try { iStack stack( 32 ); // ok: declaration in try block stack.display(); for ( int ix = 1; ix < 51; ++ix ) { // same as before } } catch ( pushOnFull ) { // cannot refer to stack here } catch ( popOnEmpty ) { // cannot refer to stack here } // cannot refer to stack here return 0; }
It is possible to declare a function so that the entire body of the function is contained within the try block. In such a case, instead of placing the try block within the function definition we can enclose the function body within a function try block. This organization supports the cleanest separation between the code that supports the normal processing of the program and the code that supports the handling of the exceptions. For example:
int main() try { iStack stack( 32 ); stack.display(); for ( int ix = 1; ix < 51; ++ix ) { // same as before } return 0; } catch ( pushOnFull ) { // cannot refer to stack here } catch ( popOnEmpty ) { // cannot refer to stack here }
Notice that the try keyword comes before the opening brace of the function body and the catch clauses are listed after the function body's closing brace. With this code organization, the code that supports the normal processing of main() is placed within the function body, clearly separated from the code that handles the exceptions in the catch clauses. However, variables declared within main()'s function body cannot be referred to within the catch clauses.
A function try block associates a group of catch clauses with a function body. If a statement within the function body throws an exception, the handlers that follow the function body are considered to handle the exception. Function try blocks are particularly useful with class constructors. We will reexamine function try blocks in this context in Chapter 19.
Exercise 11.3
Write a program that defines an IntArray object (where IntArray is the class type defined in Section 2.3) and performs the following actions. We have three files containing integer values.
Read the first file and assign the first, third, fifth, ..., nth value read (where n is an odd number) to the IntArray object; then display the content of the IntArray object.
Read the second file and assign the fifth, tenth, ..., nth value read (where n is a multiple of 5) to the IntArray object; then display the content of the IntArray object.
Read the third file and assign the second, fourth, sixth..., nth value read (where n is an even number) to the IntArray object; then display the content of the Int- Array object.
Use the IntArray operator[]() defined in Exercise 11.2 to store values into and read values from the IntArray object. Because operator[]() may throw an exception, use one or more try blocks and catch clauses in your program to handle the possible exceptions thrown by operator[]() . Explain the reasoning behind where you located the try blocks in your program.
11.3 Catching an Exception
A C++ exception handler is a catch clause. When an exception is thrown from statements within a try block, the list of catch clauses that follows the try block is searched to find a catch clause that can handle the exception.
A catch clause consists of three parts: the keyword catch, the declaration of a single type or single object within parentheses (referred to as an exception declaration), and a set of statements within a compound statement. If the catch clause is selected to handle an exception, the compound statement is executed. Let's examine the catch clauses for the exceptions pushOnFull and popOnEmpty in the function main() in more detail.
catch ( pushOnFull ) { cerr << "trying to push a value on a full stack\n"; return errorCode88; } catch ( popOnEmpty ) { cerr << "trying to pop a value on an empty stack\n"; return errorCode89; }
Both catch clauses have an exception declaration of class type; the first one is of type pushOnFull, and the second one is of type popOnEmpty. A handler is selected to handle an exception if the type of its exception declaration matches the type of the exception thrown. (We will see in Chapter 19 that the types do not have to match exactly: a handler for a base class can handle exceptions of a class type derived from the type of the handler's exception declaration.) For example, when the pop() member function of the class iStack throws a popOnEmpty exception, the second catch clause is entered. After an error message is issued to cerr, the function main() returns errorCode89.
If these catch clauses do not contain a return statement, where does the execution of the program continue? After a catch clause has completed its work, the execution of the program continues at the statement that follows the last catch clause in the list. In our example, the execution of the program continues with the return statement in main() and, after the catch clause for popOnEmpty generates an error message to cerr, main() returns the value 0.
int main() { iStack stack( 32 ); try { stack.display(); for ( int ix = 1; ix < 51; ++ix ) { // same as before } } catch ( pushOnFull ) { cerr << "trying to push a value on a full stack\n"; } catch ( popOnEmpty ) { cerr << "trying to pop a value on an empty stack\n"; } // execution of the program continues here return 0; }
The C++ exception handling mechanism is said to be nonresumptive; once the exception has been handled, the execution of the program does not resume where the exception was originally thrown. In our example, once the exception has been handled, the execution of the program does not continue in the pop() member function where the exception was thrown.
11.3.1 Exception Objects
The exception declaration of a catch clause can be either a type declaration or an object declaration. When should the exception declaration in a catch clause declare an object? An object should be declared when it is necessary to obtain the value or manipulate the exception object created by the throw expression. If we design our exception classes to store information in the exception object when the exception is thrown and if the exception declaration of the catch clause declares an object, the statements within the catch clause can use this object to refer to the information stored by the throw expression.
For example, let's change the design of the pushOnFull exception class. Let's store within the exception object the value that cannot be pushed on the stack. The catch clause is changed to display this value when the error message is generated to cerr. To do this, we first need to change the definition of the pushOnFull class type. Here is our new definition:
// new exception class: // it stores the value that cannot be pushed on the stack class pushOnFull { public: pushOnFull( int i ) : _value( i ) { } int value() { return _value; } private: int _value; };
The new private data member _value holds the value that cannot be pushed on the stack. The constructor takes a value of type int and stores this value in the _value data member. Here is how the constructor can be invoked by the throw expression to store in the exception object the value that cannot be pushed on the stack:
void iStack::push( int value ) { if ( full() ) // value stored in exception object throw pushOnFull( value ); // ... }
The class pushOnFull also has a new member function value() that can be used in the catch clause to display the value stored in the exception object. Here is how it can be used:
catch ( pushOnFull eObj ) { cerr << "trying to push the value " << eObj.value() << " on a full stack\n"; }
Notice that the catch clause's exception declaration declares the object eObj, which is used to invoke the member function value() of the class pushOnFull.
An exception object is always created at the throw point even though the throw expression is not a constructor call and even though it doesn't appear to be creating an exception object. For example:
enum EHstate { noErr, zeroOp, negativeOp, severeError }; enum EHstate state = noErr; int mathFunc( int i ) { if ( i == 0 ) { state = zeroOp; throw state; // exception object created } // otherwise, normal processing continues }
In this example, the object state is not used as the exception object. Instead, an exception object of type EHstate is created by the throw expression and initialized with the value of the global object state. How can a program tell that the exception object is distinct from the global object state? To answer this question we must first look at the catch clause exception declaration in more detail.
The exception declaration of a catch clause behaves very much like a parameter declaration. When a catch clause is entered, if the exception declaration declares an object, this object is initialized with a copy of the exception object. For example, the following function calculate() calls the function mathFunc() defined earlier. When the catch clause in calculate() is entered, the object eObj is initialized with a copy of the exception object created by the throw expression:
void calculate( int op ) { try { mathFunc( op ); } catch ( EHstate eObj ) { // eObj is a copy of the exception object thrown } }
The exception declaration in this example resembles a pass-by-value parameter. The object eObj is initialized with the value of the exception object in the same way that a pass-by-value function parameter is initialized with the value of the corresponding argument (pass-by-value parameters are discussed in Section 7.3).
As is the case for function parameters, the exception declaration of a catch clause can be changed to a reference declaration. The catch clause then directly refers to the exception object created by the throw expression instead of creating a local copy. For example:
void calculate( int op ) { try { mathFunc( op ); } catch ( EHstate &eObj ) { // eObj refers to the exception object thrown } }
For the same reasons that parameters of class type should be declared as references to prevent unnecessary copying of large class objects, it is also preferable if exception declarations for exceptions of class type are declared as references.
With an exception declaration of reference type, the catch clause is able to modify the exception object. However, any variable specified by the throw expression remains unaffected. For example, modifying eObj within the catch clause does not affect the global variable state specified by the throw expression:
void calculate( int op ) { try { mathFunc( op ); } catch ( EHstate &eObj ) { // fix exception situation eObj = noErr; // global variable state is not modified } }
The catch clause resets eObj to the value noErr after correcting the exception situation. Because eObj is a reference, we can expect this assignment to modify the global object state. However, the assignment modifies only the exception object created by the throw expression. And because the exception object is a distinct object from the global object state, state remains unchanged by the modification to eObj in the catch clause.
11.3.2 Stack Unwinding
The search for a catch clause to handle a thrown exception proceeds as follows: if the throw expression is located within a try block, the catch clauses associated with this try block are examined to see whether one of these clauses can handle the exception. If a catch clause is found, the exception is handled. If no catch clause is found, the search continues in the calling function. If the call to the function exiting with the thrown exception is located within a try block, the catch clauses associated with this try block are examined to see whether one can handle the exception. If a catch clause is found, the exception is handled. If no catch clause is found, the search continues in the calling function. This process continues up the chain of nested function calls until a catch clause for the exception is found. As soon as a catch clause that can handle the exception is encountered, the catch clause is entered and the execution of the program continues within this handler.
In our example, the first function that is searched for a catch clause is the pop() member function of the class iStack. Because the throw expression in pop() is not located within a try block, pop() exits with an exception. The next function examined is the function that calls the member function pop(), which is main() in our example. The call of pop() in main() is located within a try block. The catch clauses associated with this try block are considered to handle the exception. A catch clause for exceptions of type popOnEmpty is found and entered to handle the exception.
The process by which compound statements and function definitions exit because of a thrown exception in the search for a catch clause to handle the exception is called stack unwinding. As the stack is unwound, the lifetime of local objects declared in the compound statements and in function definitions that are exited ends. C++ guarantees that, as the stack is unwound, the destructors for local class objects are called even though their lifetime ends because of a thrown exception. We look into this in more detail in Chapter 19.
What if the program does not provide a catch clause for the exception that is thrown? An exception cannot remain unhandled. An exception is an important enough situation that the program cannot continue executing normally. If no handler is found, the program calls the terminate() function defined in the C++ standard library. The default behavior of terminate() is to call abort(), indicating the abnormal exit from the program. (In most situations, calling abort() is good enough. However, in some cases it is necessary to override the actions performed by terminate(). [STROUSTRUP94] shows how this can be done and discusses it in more detail.)
By now, you will probably have noticed many similarities between exception handling and function calls. A throw expression behaves somewhat like a function call, and the catch clause behaves somewhat like a function definition. The main difference between these two mechanisms is that all the information necessary to set up a function call is available at compile-time, and that is not true for the exception handling mechanism. C++ exception handling requires run-time support. For example, for an ordinary function call, the compiler knows at the point of the call which function will actually be called through the process of function overload resolution. For exception handling, the compiler does not know for a particular throw expression in which function the catch clause resides and where execution resumes after the exception has been handled. These decisions happen at run-time. The compiler cannot inform users when no handler exists for an exception. This is why the terminate() function exists: it is a run-time mechanism to tell users when no handler matches the exception thrown.
11.3.3 Rethrow
It is possible that a single catch clause cannot completely handle an exception. After some corrective actions, a catch clause may decide that the exception must be handled by a function further up the list of function calls. A catch clause can pass the exception to another catch clause further up the list of function calls by rethrowing the exception. A rethrow expression has this form:
throw;
A rethrow expression rethrows the exception object. A rethrow can appear only in the compound statement of a catch clause. For example:
catch ( exception eObj ) { if ( canHandle( eObj ) ) // handle the exception return; else // rethrow it for another catch clause to handle throw; }
The exception that is rethrown is the original exception object. This has some implications if the catch clause modifies the exception object before rethrowing it. The following does not modify the original exception object. Can you see why?
enum EHstate { noErr, zeroOp, negativeOp, severeError }; void calculate( int op ) { try { // exception thrown by mathFunc() has value zeroOp mathFunc( op ); } catch ( EHstate eObj ) { // fix a few things // attempt to modify the exception object eObj = severeErr; // intends to rethrow an exception of value severeErr throw; } }
Because eObj is not a reference, the catch clause receives a copy of the exception object, and any modifications to eObj within the handler modify the local copy. They do not affect the original exception object created by the throw expression. It is this original exception object that is rethrown by the rethrow expression. Because the original exception is not modified within the catch clause in our example, the object rethrown still has its initial zeroOp value.
To modify the original exception object, the exception declaration within the catch clause must declare a reference. For example:
catch ( EHstate &eObj ) { // modifies the exception object eObj = severeErr; // The value of the exception rethrown is severeErr throw; }
eObj refers to the exception object created by the throw expression, and modifications to eObj in the catch clause affect the original exception object. These modifications are part of the exception object that is rethrown.
Therefore, another good reason to declare the exception declaration of the catch clause as a reference is to ensure that modifications applied to the exception object within the catch clause are reflected in the exception object that is rethrown. We will see another good reason why exception declarations for exceptions of class type should be references in Section 19.2, where we see how catch clauses can invoke the class virtual functions.
11.3.4 The Catch-All Handler
A function may want to perform some action before it exits with a thrown exception even though it cannot handle the exception that is thrown. For example, a function may acquire some resource, such as opening a file or allocating memory on the heap, and it may want to release this resource (close the file or release the memory) before it exits with the thrown exception. For example:
void manip() { resource res; res.lock(); // locks a resource // use res // some action that causes an exception to be thrown res.release(); // skipped if exception thrown }
The release of the resource res is bypassed if an exception is thrown. To guarantee that the resource is released, rather than provide a specific catch clause for every possible exception and because we can't know all the exceptions that might be thrown, we can use a catch-all catch clause. This catch clause has an exception declaration of the form (...) , where the three dots are referred to as an ellipsis. This catch clause is entered for any type of exception. For example:
// entered with any exception thrown catch ( ... ) { // place our code here }
A catch(...) is used in combination with a rethrow expression. The resource that has been locked is released within the compound statement of the catch clause before the exception is propagated further up the list of function calls with a rethrow expression:
void manip() { resource res; res.lock(); try { // use res // some action that causes an exception to be thrown } catch (...) { res.release(); throw; } res.release(); // skipped if exception thrown }
To ensure that the resource is appropriately released if an exception is thrown and manip() exits with an exception, a catch(...) is used to release the resource before the exception is propagated to functions further up the list of function calls. We can also manage the acquisition and release of a resource by encapsulating the resource in a class, and having the class constructor acquire the resource and the class destructor release the resource automatically. We look at how to do this in Chapter 19.
A catch (...) clause can be used by itself or in combination with other catch clauses. If it is used in combination with other catch clauses, we must take some care when organizing the set of catch clauses associated with the try block.
Catch clauses are examined in turn, in the order in which they appear following the try block. Once a match is found, subsequent catch clauses are not examined. This implies that if a catch (...) is used in combination with other catch clauses, it must always be placed last in a list of exception handlers; otherwise, a compiler error is issued. For example:
try { stack.display(); for ( int ix = 1; ix < 51; ++ix ) { // same as before } } catch ( pushOnFull ) { } catch ( popOnEmpty ) { } catch (...) { } // must be last catch clause
Exercise 11.4
Explain why we say that the C++ exception handling model is nonresumptive.
Exercise 11.5
Given the following exception declarations, provide a throw expression that creates an exception object that can be caught by the following catch clauses.
(a) class exceptionType { }; catch( exceptionType *pet ) { } (b) catch(...) { } (c) enum mathErr { overflow, underflow, zeroDivide }; catch( mathErr &ref ) { } (d) typedef int EXCPTYPE; catch( EXCPTYPE ) { }
Exercise 11.6
Explain what happens during stack unwinding.
Exercise 11.7
Give two reasons that the exception declaration of a catch clause should declare a reference.
Exercise 11.8
Using the code you developed for Exercise 11.3, modify the exception class you created so that the invalid index used with operator[]() is stored in the exception object when the exception is thrown and later displayed by the catch clause. Modify your program so that operator[]() throws an exception during the execution of the program.
11.4 Exception Specifications
It is not possible, looking at the declarations for the member functions pop() and push() of the class iStack, to determine that these functions may throw exceptions. One possible solution is to add a comment associated with the declaration of each member function. In this way, the class interface that appears in the header file also documents the exceptions the class member functions may throw:
class iStack { public: // ... void pop( int &value ); // throw popOnEmpty void push( int value ); // throw pushOnFull private: // ... };
However, this is less than ideal. There is no guarantee that this documentation will remain up-to-date with later releases of our iStack class. Nor does it provide information to the compiler to guarantee that no other kinds of exceptions are thrown. An exception specification provides a solution that can be used to list the exceptions a function may throw with the function declaration. It guarantees that the function does not throw any other types of exceptions.
An exception specification follows the function parameter list. It is specified with the keyword throw followed by a list of exception types enclosed in parentheses. For example, the declarations of the member functions of the class iStack can be modified as follows to add the appropriate exception specifications:
class iStack { public: // ... void pop( int &value ) throw(popOnEmpty); void push( int value ) throw(pushOnFull); private: // ... };
A call of pop() guarantees not to throw any exception other than an exception of type popOnEmpty. Similarly, a call of push() guarantees not to throw any exception other than an exception of type pushOnFull.
An exception declaration is part of the function's interface, and it must be specified on the function declarations that appear in header files. An exception specification is a contract between the function and the rest of the program. It is a guarantee that the function will not throw any exception not listed in its exception specification.
If a function declaration specifies an exception specification, a redeclaration of the same function must specify an exception specification with the same types. The exception specifications on different declarations of the same function are not cumulative. For example:
// two declarations of the same function extern int foo( int = 0 ) throw(string); // error: exception specification omitted extern int foo( int parm ) { }
What happens if the function throws an exception that is not listed in its exception specification? Exceptions are thrown only if certain program anomalies are encountered, and it is not possible to know at compile- time whether a program encounters these exceptions at run-time. Therefore, violations of a function's exception specification can be detected only at run- time. If a function throws an exception not listed in its exception specification, the function unexpected(), defined in the C++ standard library, is invoked. The default behavior of unexpected() is to call terminate(). (In some situations, it may be necessary to override the actions performed by unexpected(). The C++ standard library provides a mechanism to override the default behavior of unexpected(). [STROUSTRUP97] discusses this in more detail.)
We should clarify that the function unexpected() is not called merely because a function throws an exception not listed in its exception specification. If the function handles the exception itself and if the exception is handled before it "escapes" outside the function, then all is fine. For example:
void recoup( int op1, int op2 ) throw(ExceptionType) { try { // ... throw string("we're in control"); } // handles the exception thrown catch ( string ) { // do whatever is needed } } // OK, unexpected() is not called
Even though an exception of type string is thrown from within the function recoup() and even though the function recoup() guarantees not to throw exceptions of a type other than ExceptionType, because the exception is handled before it escapes from the function recoup(), the function unexpected() is not called as a result of this exception being thrown.
Violations of a function's exception specification are detected only at run-time. The compiler does not generate compile-time errors if an expression can throw an exception of a type disallowed by the exception specification. If this expression is never executed or if it never throws the exception that is violating the exception specification, the program runs as expected and the function exception specification is never violated. For example:
extern void doit( int, int ) throw(string, exceptionType); void action (int op1, int op2 ) throw(string) { doit( op1, op2 ); // no compile-time error // ... }
The function doit() can throw an exception of type exceptionType, which is not allowed by the exception specification of the function action() . Even though this type of exception is not allowed by the function action(), this function compiles successfully. Instead, the compiler generates code to ensure that if an exception violating the exception specification is thrown, the run-time library function unexpected() is called.
An empty exception specification guarantees that the function does not throw any exception. For example, the function no_problem() guarantees not to throw an exception:
extern void no_problem() throw();
If a function declaration does not specify an exception specification, the function can throw exceptions of any type.
No type conversion is allowed between the type of the exception thrown and a type specified by the exception specification. For example:
int convert( int parm ) throw(string) { // ... if ( somethingRather ) // program error: // convert() does not allow exception of type const char* throw "help!"; }
The throw expression in the function convert() throws a C-style character string. The exception object created by this throw expression has type const char*. Usually, an expression of type const char* can be converted to the type string. However, an exception specification does not allow type conversions from the type of the exception thrown to the type specified by the exception specification. If convert() throws this exception, the function unexpected() is called. To correct this situation, the throw expression can be modified to explicitly convert the value of the expression to the type string as follows:
throw string( "help!" );
11.4.1 Exception Specifications and Pointers to Functions
An exception specification can also be provided in the declaration of a pointer to function. For example:
void (*pf)( int ) throw(string);
This declaration indicates that pf points to a function that can throw exceptions only of type string. As with function declarations, the exception specifications of different declarations for the same pointer to function are not cumulative, and all the declarations for the pointer pf must specify the same exception specification. For example:
extern void (*pf)( int ) throw(string); // error: missing exception specification void (*pf)( int );
When a pointer to function with an exception specification is initialized (or assigned to), there are restrictions on the type of the pointer used as the initializer (or used as the rvalue on the right-hand side of the assignment). The exception specifications of both pointers do not have to be identical. However, the exception specification of the pointer used as the initializer or rvalue must be either as restrictive as or more restrictive than the exception specification of the pointer that is initialized or assigned to. For example:
void recoup( int, int ) throw(exceptionType); void no_problem() throw(); void doit( int, int ) throw(string, exceptionType); // ok: recoup() is as restrictive as pf1 void (*pf1)( int, int ) throw(exceptionType) = &recoup; // ok: no_problem() is more restrictive than pf2 void (*pf2)() throw(string) = &no_problem; // error: doit() is less restrictive than pf3 void (*pf3)( int, int ) throw(string) = &doit;
The third initialization does not make sense. The pointer declaration guarantees that pf3 points to a function that will not throw any exceptions except those of type string. However, the function doit() may also throw an exception of type exceptionType. Because the function doit() does not satisfy the guarantee of the exception specification of pf3, the function doit() is not a valid initializer for pf3 and a compiler error is issued.
Exercise 11.9
Using the code you developed for Exercise 11.8, change the declaration of class IntArray operator[]() to add an appropriate exception specification to describe the exception this operator can throw. Modify your program so that operator[]() throws an exception not listed in its exception specification. What happens then?
Exercise 11.10
What exceptions can a function throw if it has an exception specification of the form throw()? If it has no exception specification?
Exercise 11.11
Which one, if any, of the following pointer assignments is in error? Why?
void example() throw(string); (a) void (*pf1)() = example; (b) void (*pf2)() throw() = example;
11.5 Exceptions and Design Issues
There are some design issues associated with the use of exception handling in C++ programs. Although the support for exception handling is built into the language, not every C++ program should use exception handling. Exception handling should be used to communicate program anomalies between parts of the program that are developed independently, because throwing an exception is not as fast as a normal function call. For example, a library implementer may decide to communicate program anomalies to users of the library using exceptions. If a library function encounters an exceptional situation that it cannot handle locally, it may throw an exception to notify the program using the library.
In our example, our library defines the iStack class and its member functions. The function main() uses the library, and we should assume that the programmer writing main() is not the library implementer. The member functions of the class iStack are capable of detecting that a pop() operation is requested on an empty stack or that a push() operation is requested on a full stack, but the library implementer does not know the state of the program that caused the pop() or push() operations to be requested in the first place and cannot write pop() and push() to locally address this situation. Because these errors cannot be handled in the member functions, we decided to throw exceptions to notify the program using the library.
Even though C++ supports exception handling, C++ programs should use other error handling techniques (such as returning an error code) when appropriate. There is no clearcut answer to the question, "When should an error become an exception?" It is really up to the library implementer to decide what an "exceptional situation" is. Exceptions are part of a library's interface, and deciding which exceptions the library throws is an important phase of the library design. If the library is intended to be used within programs that cannot afford to crash, then the library must either handle the problem itself, or, if it can't, it must communicate program anomalies to the part of the program that uses the library and give the caller the choice as to which action should be taken when no meaningful action can be taken within the library code itself. Deciding what should be handled as an exception is a difficult part of the library design.
In our iStack example, it is debatable whether the push() member function should throw an exception if the stack is full. Another, some people would say better, implementation of push() is to handle this situation locally and grow the stack if it is full. After all, the only real limit is the memory available to our program. Our decision to throw an exception if the program attempts to push a value on a full stack may have been ill-considered. We can reimplement the member function push() to grow the stack if a request is made to push a value on a full stack:
void iStack::push( int value ) { // if full, grow the underlying vector if ( full() ) _stack.resize( 2 * _stack.size() ); _stack[ _top++ ] = value; }
Similarly, should pop() throw an exception when a request is made to pop a value from an empty stack? One interesting observation is that the stack class of the C++ standard library (introduced in Chapter 6), does not throw an exception if a pop operation is requested and the stack is empty. Instead, the operation has undefined behavior: it is unknown what the program behavior is after such an operation has been requested. When the C++ standard library was designed, it was decided that an exception should not be thrown in this case. Allowing the program to continue running while an illegal state had been encountered was deemed appropriate in this situation. As we mentioned, different libraries will have different exceptions. There is no right answer to the question of what constitutes an exception.
Not all programs should worry about exceptions thrown by libraries. Although it is true that some systems cannot afford down time and should be built to handle exceptional situations, not every program has such requirements. Exception handling is primarily an aid to the implementation of fault-tolerant systems. Again, deciding whether our programs are to handle exceptions thrown from libraries or whether we should let the program terminate is a difficult part of the design process.
One last aspect of program design with exception handling is that the handling of exceptions in a program is usually layered. A program is usually built of components, and each component must decide which exceptions it will handle locally and which exceptions it will pass to higher levels of the program. What do we mean by component? For example, the text query system introduced in Chapter 6 can be broken into three components or layers. The first layer is the C++ standard library, which provides support for the basic operations on strings, maps, and so on. The second layer is the text query system itself, which defines functions, such as string_caps() and suffix_text(), that manipulate the text to be processed and uses the C++ standard library as a subcomponent. The third layer is the program that uses our text query system. Each component or layer is built independently, and each must decide which exceptional situation it will handle directly and which exceptions it will pass to higher levels of the program.
Not every function in a component or layer should be capable of dealing with exceptions. Usually, try blocks and associated catch clauses are used by functions that are the entry points into a program component. The catch clauses are designed to handle exceptions that the component does not want to let propagate to higher levels of the program. Exception specifications (discussed in Section 11.4) are also used with the functions that are the entry points into a component to guard against the escape of unwanted exceptions to higher levels of the program.
We look at other aspects of designing programs with exceptions in Chapter 19, after classes and class hierarchies have been introduced.