You can think of a throw expression as a call to a special system function that takes the exception object as an argument and backtracks up the chain of execution. For this to work, extra information needs to be put on the stack by the compiler, to aid in stack unwinding. To understand this, you need to know about the runtime stack. Whenever a function is called, information about that function is pushed onto the runtime stack in an activation record instance (ARI), also called a stack frame. A typical stack frame contains the address of the calling function (so execution can return to it), a pointer to the ARI of the function’s static parent (the scope that lexically contains the called function, so variables global to the function can be accessed), and a pointer to the function that called it (its dynamic parent). The path that logically results from repetitively following the dynamic parent links is the dynamic chain, or call chain, that we’ve mentioned previously in this chapter. This is how execution can backtrack when an exception is thrown, and it is the mechanism that makes it possible for components developed without knowledge of one another to communicate errors at runtime.
To enable stack unwinding for exception handling, extra exception-related information about each function needs to be available for each stack frame. This information describes which destructors need to be called (so that local objects can be cleaned up), indicates whether the current function has a try block, and lists which exceptions the associated catch clauses can handle. Naturally there is space penalty for this extra information, so programs that support exception handling can be somewhat larger than those that don’t.[9]Even the compile-time size of programs using exception handling is greater, since the logic of how to generate the expanded stack frames during runtime must be generated by the compiler.
To illustrate this, we compiled the following program both with and without exception-handling support in Borland C++ Builder and Microsoft Visual C++[10].
struct HasDestructor {
~HasDestructor(){}
};
void g(); // for all we know, g may throw
void f() {
HasDestructor h;
g();
}
If exception handling is enabled, the compiler must keep information about ~HasDestructor( ) available at runtime in the ARI for f( ) (so it can destroy h properly should g( ) throw an exception). The following table summarizes the result of the compilations in terms of the size of the compiled (.obj) files (in bytes).
Compiler\Mode | With Exception Support | Without Exception Support |
Borland | 616 | 234 |
Microsoft | 1162 | 680 |
Don’t take the percentage differences between the two modes too seriously. Remember that exceptions (should) typically constitute a small part of a program, so the space overhead tends to be much smaller (usually between 5 and 15 percent).
You might think that this extra housekeeping would slow down execution, and you’d be correct. A clever compiler implementation can avoid that cost, however. Since information about exception-handling code and the offsets of local objects can be computed once at compile time, such information can be kept in a single place associated with each function, but not in each ARI. You essentially remove exception overhead from each ARI and, therefore, avoid the extra time to push them onto the stack. This approach is called the zero-cost model[11] of exception handling, and the optimized storage mentioned earlier is known as the shadow stack.[12]
Summary
Error recovery is a fundamental concern for every program you write, and it’s especially important in C++, in which one of the goals is to create program components for others to use. To create a robust system, each component must be robust.
The goals for exception handling in C++ are to simplify the creation of large, reliable programs using less code than currently possible, with more confidence that your application doesn’t have an unhandled error. This is accomplished with little or no performance penalty and with low impact on existing code.
Basic exceptions are not terribly difficult to learn; begin using them in your programs as soon as you can. Exceptions are one of those features that provide immediate and significant benefits to your project.
Exercises
4. Create a class with member functions that throw exceptions. Within this class, make a nested class to use as an exception object. It takes a single char* as its argument; this represents a description string. Create a member function that throws this exception. (State this in the function’s exception specification.) Write a try block that calls this function and a catch clause that handles the exception by displaying its description string.
5. Rewrite the Stash class from Chapter 13 of Volume 1 so that it throws out_of_range exceptions for operator[].
6. Write a generic main( ) that takes all exceptions and reports them as errors.
7. Create a class with its own operator new. This operator should allocate ten objects, and on the eleventh object "run out of memory" and throw an exception. Also add a static member function that reclaims this memory. Now create a main( ) with a try block and a catch clause that calls the memory-restoration routine. Put these inside a while loop, to demonstrate recovering from an exception and continuing execution.
8. Create a destructor that throws an exception, and write code to prove to yourself that this is a bad idea by showing that if a new exception is thrown before the handler for the existing one is reached, terminate( ) is called.
9. Prove to yourself that all exception objects (the ones that are thrown) are properly destroyed.
10. Prove to yourself that if you create an exception object on the heap and throw the pointer to that object, it will not be cleaned up.
9
This depends, of course, on how much checking of return codes you would have to insert if you weren’t using exceptions.
10
Borland enables exceptions by default; to disable exceptions use the -x- compiler option. Microsoft disables support by default; to turn it on, use the -GX option. With both compilers use the -c option to compile only.
11
The GNU C++ compiler uses the zero-cost model by default. Metrowerks Code Warrior for C++ also has an option to use the zero-cost model.
12
Thanks to Scott Meyers and Josee Lajoie for their insights on the zero-cost model. You can find more information on how exceptions work in Josee’s excellent article, "Exception Handling: Behind the Scenes,"