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.
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.
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 try
s. 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 try
s 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.
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.
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.
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 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.
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
throw
s?(a)
range_error r("error");
throw r;
(b)
exception *p = &r;
throw *p;
What would happen if the
throw
in (b) were written asthrow 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.
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.
Ordinarily, a
catch
that takes an exception of a type related by inheritance ought to define its parameter as a reference.
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 non
const
toconst
are allowed. That is, athrow
of a nonconst
object can match acatch
specified to take a reference toconst
.
• 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.
Multiple
catch
clauses with types related by inheritance must be ordered from most derived type to least derived.
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
}
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.
If a
catch(...)
is used in combination with othercatch
clauses, it must be last. Anycatch
that follows a catch-all can never be matched.
try
Blocks and ConstructorsIn 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) { /* ... */ }Figure 18.1. Standard
exception
Class HierarchyExercise 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 headercstdlib
) to terminatemain
.Exercise 18.6: Given the following exception types and
catch
clauses, write athrow
expression that creates an exception object that can be caught by eachcatch
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.
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
andBlobPtr
classes from Chapter 16 to use functiontry
blocks for their constructors.
noexcept
Exception SpecificationIt 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.
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.
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.
The compiler in general cannot, and does not, verify exception specifications at compile time.
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. Thethrow
specifier appeared in the same place as thenoexcept
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 declarationThese declarations of
recoup
are equivalent. Both say thatrecoup
won’t throw.
noexcept
SpecificationThe 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
noexcept
OperatorArguments 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 sizeof
(§ 4.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.
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 thebool
argument to anoexcept
exception specifier.
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.
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.
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.
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?