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