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.
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 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.