Team LiB
Previous Section Next Section

18.1. Exception Handling

 

Exception handling allows independently developed parts of a program to communicate about and handle problems that arise at run time. Exceptions let us separate problem detection from problem resolution. One part of the program can detect a problem and can pass the job of resolving that problem to another part of the program. The detecting part need not know anything about the handling part, and vice versa.

 

In § 5.6 (p. 193) we introduced the basic concepts and mechanics of using exceptions. In this section we’ll expand our coverage of these basics. Effective use of exception handling requires understanding what happens when an exception is thrown, what happens when it is caught, and the meaning of the objects that communicate what went wrong.

 

18.1.1. Throwing an Exception

 

In C++, an exception is raised by throwing an expression. The type of the thrown expression, together with the current call chain, determines which handler will deal with the exception. The selected handler is the one nearest in the call chain that matches the type of the thrown object. The type and contents of that object allow the throwing part of the program to inform the handling part about what went wrong.

 

When a throw is executed, the statement(s) following the throw are not executed. Instead, control is transferred from the throw to the matching catch. That catch might be local to the same function or might be in a function that directly or indirectly called the function in which the exception occurred. The fact that control passes from one location to another has two important implications:

 

• Functions along the call chain may be prematurely exited.

 

• When a handler is entered, objects created along the call chain will have been destroyed.

 

Because the statements following a throw are not executed, a throw is like a return: It is usually part of a conditional statement or is the last (or only) statement in a function.

 
Stack Unwinding
 

When an exception is thrown, execution of the current function is suspended and the search for a matching catch clause begins. If the throw appears inside a try block, the catch clauses associated with that try are examined. If a matching catch is found, the exception is handled by that catch. Otherwise, if the try was itself nested inside another try, the search continues through the catch clauses of the enclosing trys. If no matching catch is found, the current function is exited, and the search continues in the calling function.

 

If the call to the function that threw is in a try block, then the catch clauses associated with that try are examined. If a matching catch is found, the exception is handled. Otherwise, if that try was nested, the catch clauses of the enclosing trys are searched. If no catch is found, the calling function is also exited. The search continues in the function that called the just exited one, and so on.

 

This process, known as stack unwinding, continues up the chain of nested function calls until a catch clause for the exception is found, or the main function itself is exited without having found a matching catch.

 

Assuming a matching catch is found, that catch is entered, and the program continues by executing the code inside that catch. When the catch completes, execution continues at the point immediately after the last catch clause associated with that try block.

 

If no matching catch is found, the program is exited. Exceptions are intended for events that prevent the program from continuing normally. Therefore, once an exception is raised, it cannot remain unhandled. If no matching catch is found, the program calls the library terminate function. As its name implies, terminate stops execution of the program.

 

Image Note

An exception that is not caught terminates the program.

 

 
Objects Are Automatically Destroyed during Stack Unwinding
 

During stack unwinding, blocks in the call chain may be exited prematurely. In general, these blocks will have created local objects. Ordinarily, local objects are destroyed when the block in which they are created is exited. Stack unwinding is no exception. When a block is exited during stack unwinding, the compiler guarantees that objects created in that block are properly destroyed. If a local object is of class type, the destructor for that object is called automatically. As usual, the compiler does no work to destroy objects of built-in type.

 

If an exception occurs in a constructor, then the object under construction might be only partially constructed. Some of its members might have been initialized, but others might not have been initialized before the exception occurred. Even if the object is only partially constructed, we are guaranteed that the constructed members will be properly destroyed.

 

Similarly, an exception might occur during initialization of the elements of an array or a library container type. Again, we are guaranteed that the elements (if any) that were constructed before the exception occurred will be destroyed.

 
Destructors and Exceptions
 

The fact that destructors are run—but code inside a function that frees a resource may be bypassed—affects how we structure our programs. As we saw in § 12.1.4 (p. 467), if a block allocates a resource, and an exception occurs before the code that frees that resource, the code to free the resource will not be executed. On the other hand, resources allocated by an object of class type generally will be freed by their destructor. By using classes to control resource allocation, we ensure that resources are properly freed, whether a function ends normally or via an exception.

 

The fact that destructors are run during stack unwinding affects how we write destructors. During stack unwinding, an exception has been raised but is not yet handled. If a new exception is thrown during stack unwinding and not caught in the function that threw it, terminate is called. Because destructors may be invoked during stack unwinding, they should never throw exceptions that the destructor itself does not handle. That is, if a destructor does an operation that might throw, it should wrap that operation in a try block and handle it locally to the destructor.

 

In practice, because destructors free resources, it is unlikely that they will throw exceptions. All of the standard library types guarantee that their destructors will not raise an exception.

 

Image Warning

During stack unwinding, destructors are run on local objects of class type. Because destructors are run automatically, they should not throw. If, during stack unwinding, a destructor throws an exception that it does not also catch, the program will be terminated.

 

 
The Exception Object
 

The compiler uses the thrown expression to copy initialize (§ 13.1.1, p. 497) a special object known as the exception object. As a result, the expression in a throw must have a complete type (§ 7.3.3, p. 278). Moreover, if the expression has class type, that class must have an accessible destructor and an accessible copy or move constructor. If the expression has an array or function type, the expression is converted to its corresponding pointer type.

 

The exception object resides in space, managed by the compiler, that is guaranteed to be accessible to whatever catch is invoked. The exception object is destroyed after the exception is completely handled.

 

As we’ve seen, when an exception is thrown, blocks along the call chain are exited until a matching handler is found. When a block is exited, the memory used by the local objects in that block is freed. As a result, it is almost certainly an error to throw a pointer to a local object. It is an error for the same reasons that it is an error to return a pointer to a local object (§ 6.3.2, p. 225) from a function. If the pointer points to an object in a block that is exited before the catch, then that local object will have been destroyed before the catch.

 

When we throw an expression, the static, compile-time type (§ 15.2.3, p. 601) of that expression determines the type of the exception object. This point is essential to keep in mind, because many applications throw expressions whose type comes from an inheritance hierarchy. If a throw expression dereferences a pointer to a base-class type, and that pointer points to a derived-type object, then the thrown object is sliced down (§ 15.2.3, p. 603); only the base-class part is thrown.

 

Image Warning

Throwing a pointer requires that the object to which the pointer points exist wherever the corresponding handler resides.

 

 

Exercises Section 18.1.1

 

Exercise 18.1: What is the type of the exception object in the following throws?

(a) range_error r("error");

 

throw r;

 

(b) exception *p = &r;

 

throw *p;

 

What would happen if the throw in (b) were written as throw p?

 

Exercise 18.2: Explain what happens if an exception occurs at the indicated point:

 

void exercise(int *b, int *e)
{
    vector<int> v(b, e);
    int *p = new int[v.size()];
    ifstream in("ints");
    // exception occurs here
}

 

Exercise 18.3: There are two ways to make the previous code work correctly if an exception is thrown. Describe them and implement them.


 

18.1.2. Catching an Exception

 

The exception declaration in a catch clause looks like a function parameter list with exactly one parameter. As in a parameter list, we can omit the name of the catch parameter if the catch has no need to access the thrown expression.

 

The type of the declaration determines what kinds of exceptions the handler can catch. The type must be a complete type (§ 7.3.3, p. 278). The type can be an lvalue reference but may not be an rvalue reference (§ 13.6.1, p. 532).

 

When a catch is entered, the parameter in its exception declaration is initialized by the exception object. As with function parameters, if the catch parameter has a nonreference type, then the parameter in the catch is a copy of the exception object; changes made to the parameter inside the catch are made to a local copy, not to the exception object itself. If the parameter has a reference type, then like any reference parameter, the catch parameter is just another name for the exception object. Changes made to the parameter are made to the exception object.

 

Also like a function parameter, a catch parameter that has a base-class type can be initialized by an exception object that has a type derived from the parameter type. If the catch parameter has a nonreference type, then the exception object will be sliced down (§ 15.2.3, p. 603), just as it would be if such an object were passed to an ordinary function by value. On the other hand, if the parameter is a reference to a base-class type, then the parameter is bound to the exception object in the usual way.

 

Again, as with a function parameter, the static type of the exception declaration determines the actions that the catch may perform. If the catch parameter has a base-class type, then the catch cannot use any members that are unique to the derived type.

 

Image Best Practices

Ordinarily, a catch that takes an exception of a type related by inheritance ought to define its parameter as a reference.

 

 
Finding a Matching Handler
 

During the search for a matching catch, the catch that is found is not necessarily the one that matches the exception best. Instead, the selected catch is the first one that matches the exception at all. As a consequence, in a list of catch clauses, the most specialized catch must appear first.

 

Because catch clauses are matched in the order in which they appear, programs that use exceptions from an inheritance hierarchy must order their catch clauses so that handlers for a derived type occur before a catch for its base type.

 

The rules for when an exception matches a catch exception declaration are much more restrictive than the rules used for matching arguments with parameter types. Most conversions are not allowed—the types of the exception and the catch declaration must match exactly with only a few possible differences:

 

• Conversions from nonconst to const are allowed. That is, a throw of a nonconst object can match a catch specified to take a reference to const.

 

• Conversions from derived type to base type are allowed.

 

• An array is converted to a pointer to the type of the array; a function is converted to the appropriate pointer to function type.

 

No other conversions are allowed to match a catch. In particular, neither the standard arithmetic conversions nor conversions defined for class types are permitted.

 

Image Note

Multiple catch clauses with types related by inheritance must be ordered from most derived type to least derived.

 

 
Rethrow
 

Sometimes a single catch cannot completely handle an exception. After some corrective actions, a catch may decide that the exception must be handled by a function further up the call chain. A catch passes its exception out to another catch by rethrowing the exception. A rethrow is a throw that is not followed by an expression:

 

throw;

 

An empty throw can appear only in a catch or in a function called (directly or indirectly) from a catch. If an empty throw is encountered when a handler is not active, terminate is called.

 

A rethrow does not specify an expression; the (current) exception object is passed up the chain.

 

In general, a catch might change the contents of its parameter. If, after changing its parameter, the catch rethrows the exception, then those changes will be propagated only if the catch’s exception declaration is a reference:

 

 

catch (my_error &eObj) {     // specifier is a reference type
    eObj.status = errCodes::severeErr; // modifies the exception object
    throw; // the status member of the exception object is severeErr
} catch (other_error eObj) { // specifier is a nonreference type
    eObj.status = errCodes::badErr;    // modifies the local copy only
    throw; // the status member of the exception object is unchanged
}

 
The Catch-All Handler
 

Sometimes we want to catch any exception that might occur, regardless of type. Catching every possible exception can be a problem: Sometimes we don’t know what types might be thrown. Even when we do know all the types, it may be tedious to provide a specific catch clause for every possible exception. To catch all exceptions, we use an ellipsis for the exception declaration. Such handlers, sometimes known as catch-all handlers, have the form catch(...). A catch-all clause matches any type of exception.

 

A catch(...) is often used in combination with a rethrow expression. The catch does whatever local work can be done and then rethrows the exception:

 

 

void manip() {
    try {
        // actions that cause an exception to be thrown
    }
    catch (...) {
        // work to partially handle the exception
        throw;
    }
}

 

A catch(...) clause can be used by itself or as one of several catch clauses.

 

Image Note

If a catch(...) is used in combination with other catch clauses, it must be last. Any catch that follows a catch-all can never be matched.

 

 

18.1.3. Function try Blocks and Constructors

 

In general, exceptions can occur at any point in the program’s execution. In particular, an exception might occur while processing a constructor initializer. Constructor initializers execute before the constructor body is entered. A catch inside the constructor body can’t handle an exception thrown by a constructor initializer because a try block inside the constructor body would not yet be in effect when the exception is thrown.

 

Exercises Section 18.1.2

 

Exercise 18.4: Looking ahead to the inheritance hierarchy in Figure 18.1 (p. 783), explain what’s wrong with the following try block. Correct it.

 

 

try {
    // use of the C++ standard library
} catch(exception) {
    // ...
} catch(const runtime_error &re) {
    // ...
} catch(overflow_error eobj) { /* ... */ }

 
Image

Figure 18.1. Standard exception Class Hierarchy

 

Exercise 18.5: Modify the following main function to catch any of the exception types shown in Figure 18.1 (p. 783):

 

 

int main() {
    // use of the C++ standard library
}

 

The handlers should print the error message associated with the exception before calling abort (defined in the header cstdlib) to terminate main.

 

Exercise 18.6: Given the following exception types and catch clauses, write a throw expression that creates an exception object that can be caught by each catch clause:

(a) class exceptionType { };

 

catch(exceptionType *pet) { }

 

(b) catch(...) { }

 

(c) typedef int EXCPTYPE;

 

catch(EXCPTYPE) { }

 

 

To handle an exception from a constructor initializer, we must write the constructor as a function try block. A function try block lets us associate a group of catch clauses with the initialization phase of a constructor (or the destruction phase of a destructor) as well as with the constructor’s (or destructor’s) function body. As an example, we might wrap the Blob constructors (§ 16.1.2, p. 662) in a function try block:

 

 

template <typename T>
Blob<T>::Blob(std::initializer_list<T> il) try :
              data(std::make_shared<std::vector<T>>(il)) {
         /* empty body */
} catch(const std::bad_alloc &e) { handle_out_of_memory(e); }

 

Notice that the keyword try appears before the colon that begins the constructor initializer list and before the curly brace that forms the (in this case empty) constructor function body. The catch associated with this try can be used to handle exceptions thrown either from within the member initialization list or from within the constructor body.

 

It is worth noting that an exception can happen while initializing the constructor’s parameters. Such exceptions are not part of the function try block. The function try block handles only exceptions that occur once the constructor begins executing. As with any other function call, if an exception occurs during parameter initialization, that exception is part of the calling expression and is handled in the caller’s context.

 

Image Note

The only way for a constructor to handle an exception from a constructor initializer is to write the constructor as a function try block.

 

 

Exercises Section 18.1.3

 

Exercise 18.7: Define your Blob and BlobPtr classes from Chapter 16 to use function try blocks for their constructors.

 

 

18.1.4. The noexcept Exception Specification

 

It can be helpful both to users and to the compiler to know that a function will not throw any exceptions. Knowing that a function will not throw simplifies the task of writing code that calls that function. Moreover, if the compiler knows that no exceptions will be thrown, it can (sometimes) perform optimizations that must be suppressed if code might throw.

 
Image

Under the new standard, a function can specify that it does not throw exceptions by providing a noexcept specification. The keyword noexcept following the function parameter list indicates that the function won’t throw:

 

 

void recoup(int) noexcept;   // won't throw
void alloc(int);             // might throw

 

These declarations say that recoup will not throw any exceptions and that alloc might. We say that recoup has a nonthrowing specification.

 

The noexcept specifier must appear on all of the declarations and the corresponding definition of a function or on none of them. The specifier precedes a trailing return (§ 6.3.3, p. 229). We may also specify noexcept on the declaration and definition of a function pointer. It may not appear in a typedef or type alias. In a member function the noexcept specifier follows any const or reference qualifiers, and it precedes final, override, or = 0 on a virtual function.

 
Violating the Exception Specification
 

It is important to understand that the compiler does not check the noexcept specification at compile time. In fact, the compiler is not permitted to reject a function with a noexcept specifier merely because it contains a throw or calls a function that might throw (however, kind compilers will warn about such usages):

 

 

// this function will compile, even though it clearly violates its exception specification
void f() noexcept       // promises not to throw any exception
{
    throw exception();  // violates the exception specification
}

 

As a result, it is possible that a function that claims it will not throw will in fact throw. If a noexcept function does throw, terminate is called, thereby enforcing the promise not to throw at run time. It is unspecified whether the stack is unwound. As a result, noexcept should be used in two cases: if we are confident that the function won’t throw, and/or if we don’t know what we’d do to handle the error anyway.

 

Specifying that a function won’t throw effectively promises the callers of the nonthrowing function that they will never need to deal with exceptions. Either the function won’t throw, or the whole program will terminate; the caller escapes responsibility either way.

 

Image Warning

The compiler in general cannot, and does not, verify exception specifications at compile time.

 

 

Backward Compatibility: Exception Specifications

Earlier versions of C++ had a more elaborate scheme of exception specifications that allowed us to specify the types of exceptions that a function might throw. A function can specify the keyword throw followed by a parenthesized list of types that the function might throw. The throw specifier appeared in the same place as the noexcept specifier does in the current language.

 

This approach was never widely used and has been deprecated in the current standard. Although these more elaborate specifiers have been deprecated, there is one use of the old scheme that is in widespread use. A function that is designated by throw() promises not to throw any exceptions:

 

 

void recoup(int) noexcept;  // recoup doesn't throw
void recoup(int) throw();   // equivalent declaration

 

These declarations of recoup are equivalent. Both say that recoup won’t throw.

 

 
Arguments to the noexcept Specification
 

The noexcept specifier takes an optional argument that must be convertible to bool: If the argument is true, then the function won’t throw; if the argument is false, then the function might throw:

 

 

void recoup(int) noexcept(true);  //  recoup won't throw
void alloc(int) noexcept(false);  //  alloc can throw

 
The noexcept Operator
 
Image

Arguments to the noexcept specifier are often composed using the noexcept operator. The noexcept operator is a unary operator that returns a bool rvalue constant expression that indicates whether a given expression might throw. Like sizeof4.9, p. 156), noexcept does not evaluate its operand.

 

For example, this expression yields true:

 

 

noexcept(recoup(i)) // true if calling recoup can't throw, false otherwise

 

because we declared recoup with a noexcept specifier. More generally,

 

noexcept(e)

 

is true if all the functions called by e have nonthrowing specifications and e itself does not contain a throw. Otherwise, noexcept(e) returns false.

 

We can use the noexcept operator to form an exception specifier as follows:

 

 

void f() noexcept(noexcept(g())); // f has same exception specifier as g

 

If the function g promises not to throw, then f also is nonthrowing. If g has no exception specifier, or has an exception specifier that allows exceptions, then f also might throw.

 

Image Note

noexcept has two meanings: It is an exception specifier when it follows a function’s parameter list, and it is an operator that is often used as the bool argument to a noexcept exception specifier.

 

 
Exception Specifications and Pointers, Virtuals, and Copy Control
 

Although the noexcept specifier is not part of a function’s type, whether a function has an exception specification affects the use of that function.

 

A pointer to function and the function to which that pointer points must have compatible specifications. That is, if we declare a pointer that has a nonthrowing exception specification, we can use that pointer only to point to similarly qualified functions. A pointer that specifies (explicitly or implicitly) that it might throw can point to any function, even if that function includes a promise not to throw:

 

 

// both recoup and pf1 promise not to throw
void (*pf1)(int) noexcept = recoup;

// ok: recoup won't throw; it doesn't matter that pf2 might
void (*pf2)(int) = recoup;

pf1 = alloc; // error: alloc might throw but pf1 said it wouldn't
pf2 = alloc; // ok: both pf2 and alloc might throw

 

If a virtual function includes a promise not to throw, the inherited virtuals must also promise not to throw. On the other hand, if the base allows exceptions, it is okay for the derived functions to be more restrictive and promise not to throw:

 

 

class Base {
public:
    virtual double f1(double) noexcept; // doesn't throw
    virtual int f2() noexcept(false);   // can throw
    virtual void f3();                  // can throw
};

class Derived : public Base {
public:
    double f1(double);        // error: Base::f1 promises not to throw
    int f2() noexcept(false); // ok: same specification as Base::f2
    void f3() noexcept;       // ok: Derived f3 is more restrictive
};

 

When the compiler synthesizes the copy-control members, it generates an exception specification for the synthesized member. If all the corresponding operation for all the members and base classes promise not to throw, then the synthesized member is noexcept. If any function invoked by the synthesized member can throw, then the synthesized member is noexcept(false). Moreover, if we do not provide an exception specification for a destructor that we do define, the compiler synthesizes one for us. The compiler generates the same specification as it would have generated had it synthesized the destructor for that class.

 

Exercises Section 18.1.4

 

Exercise 18.8: Review the classes you’ve written and add appropriate exception specifications to their constructors and destructors. If you think one of your destructors might throw, change the code so that it cannot throw.


 

18.1.5. Exception Class Hierarchies

 

The standard-library exception classes (§ 5.6.3, p. 197) form the inheritance hierarchy (Chapter 15) as shown in Figure 18.1.

 

The only operations that the exception types define are the copy constructor, copy-assignment operator, a virtual destructor, and a virtual member named what. The what function returns a const char* that points to a null-terminated character array, and is guaranteed not to throw any exceptions.

 

The exception, bad_cast, and bad_alloc classes also define a default constructor. The runtime_error and logic_error classes do not have a default constructor but do have constructors that take a C-style character string or a library string argument. Those arguments are intended to give additional information about the error. In these classes, what returns the message used to initialize the exception object. Because what is virtual, if we catch a reference to the base-type, a call to the what function will execute the version appropriate to the dynamic type of the exception object.

 
Exception Classes for a Bookstore Application
 

Applications often extend the exception hierarchy by defining classes derived from exception (or from one of the library classes derived from exception). These application-specific classes represent exceptional conditions specific to the application domain.

 

If we were building a real bookstore application, our classes would have been much more complicated than the ones presented in this Primer. One such complexity would be how these classes handled exceptions. In fact, we probably would have defined our own hierarchy of exceptions to represent application-specific problems. Our design might include classes such as

 

 

// hypothetical exception classes for a bookstore application
class out_of_stock: public std::runtime_error {
public:
    explicit out_of_stock(const std::string &s):
                       std::runtime_error(s) { }
};
class isbn_mismatch: public std::logic_error {
public:
    explicit isbn_mismatch(const std::string &s):
                          std::logic_error(s) { }
    isbn_mismatch(const std::string &s,
        const std::string &lhs, const std::string &rhs):
        std::logic_error(s), left(lhs), right(rhs) { }
    const std::string left, right;
};

 

Our application-specific exception types inherit them from the standard exception classes. As with any hierarchy, we can think of the exception classes as being organized into layers. As the hierarchy becomes deeper, each layer becomes a more specific exception. For example, the first and most general layer of the hierarchy is represented by class exception. All we know when we catch an object of type exception is that something has gone wrong.

 

The second layer specializes exception into two broad categories: run-time or logic errors. Run-time errors represent things that can be detected only when the program is executing. Logic errors are, in principle, errors that we could have detected in our application.

 

Our bookstore exception classes further refine these categories. The class named out_of_stock represents something, particular to our application, that can go wrong at run time. It would be used to signal that an order cannot be fulfilled. The class isbn_mismatch represents a more particular form of logic_error. In principle, a program could prevent and handle this error by comparing the results of isbn() on the objects.

 
Using Our Own Exception Types
 

We use our own exception classes in the same way that we use one of the standard library classes. One part of the program throws an object of one of these types, and another part catches and handles the indicated problem. As an example, we might define the compound addition operator for our Sales_data class to throw an error of type isbn_mismatch if it detected that the ISBNs didn’t match:

 

 

// throws an exception if both objects do not refer to the same book
Sales_data&
Sales_data::operator+=(const Sales_data& rhs)
{
    if (isbn() != rhs.isbn())
        throw isbn_mismatch("wrong isbns", isbn(), rhs.isbn());
    units_sold += rhs.units_sold;
    revenue += rhs.revenue;
    return *this;
}

 

Code that uses the compound addition operator (or ordinary addition operator, which itself uses the compound addition operator) can detect this error, write an appropriate error message, and continue:

 

 

// use the hypothetical bookstore exceptions
Sales_data item1, item2, sum;
while (cin >> item1 >> item2) {  // read two transactions
    try {
        sum = item1 + item2;     //  calculate their sum
        // use sum
    } catch (const isbn_mismatch &e) {
      cerr << e.what() << ": left isbn(" << e.left
           << ") right isbn(" << e.right << ")" << endl;
    }
}

 

Exercises Section 18.1.5

 

Exercise 18.9: Define the bookstore exception classes described in this section and rewrite your Sales_data compound assigment operator to throw an exception.

Exercise 18.10: Write a program that uses the Sales_data addition operator on objects that have differing ISBNs. Write two versions of the program: one that handles the exception and one that does not. Compare the behavior of the programs so that you become familiar with what happens when an uncaught exception occurs.

Exercise 18.11: Why is it important that the what function doesn’t throw?


 
Team LiB
Previous Section Next Section