Exception Handling in C++
- Throwing an Exception
- The Try Block
- Catching an Exception
- Exception Specifications
- Exceptions and Design Issues
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.