The C++ feature that wasnt.
As we consider work now underway on the new C++ Standard, C++0x, its a good time to take stock of what were doing with, and have learned from, our experience with the current C++98 Standard. The vast majority of Standard C++s features are good, and they get the lions share of the print because theres not much point harping on the weaker features. Rather, the weaker and less useful features more often just get ignored and atrophy from disuse until many people forget theyre even there (not always a bad thing). Thats why youve seen relatively few articles about obscure features like valarray, bitset, locales, and the legal expression 5[a] and the same is true, for reasons which we shall see in this column and the next, for exception specifications and export.
This time, lets take a closer look at the state of our experience with Standard C++ exception specifications.
The Story So Far
The idea behind exception specifications is easy to understand: in a C++ program, unless otherwise specified, any function might conceivably emit any type of exception. Consider a function named Func() (because the name f() is so dreadfully overused):
// Example 1(a) // // can throw anything int Func();
By default, in C++, Func() could indeed throw anything, just as the comment says. Now, often we know just what kinds of things a function might throw, and then its certainly reasonable to want to supply the compiler and the human programmer with some information limiting what exceptions could come tearing out of a function. For example:
// Example 1(b) // // will throw nothing int Gunc() throw(); // can only throw A or B int Hunc() throw(A,B);
In these cases, the functions exception specification exists in order to say something about what the functions Gunc() and Hunc() could emit. The comments document colloquially what the specifications say. Well return to that colloquially part in a moment, because as it turns out these two comments are deceptively close to being correct.
One might naturally think that making a statement about what the functions might throw would be a good thing, that more information is better. One would not necessarily be right, because the devil is in the details: although the motivation is noble, the way exception specifications are, well, specified in C++ isnt always useful and can often be downright detrimental.
Issue the First: A Shadow Type System
John Spicer, of Edison Design Group fame [1] and an author of large swathes of the template chapter of the C++ Standard, has been known to call C++s exception specifications a shadow type system. One of C++s strongest features is its strong type system, and thats well and good. Why would we call exception specifications a shadow type system instead of just part of the type system?
The reason is simple, and twofold:
a) Exception specifications dont participate in a functions type.
b) Except when they do.
Consider first an example of when exception specifications dont participate in a functions type. Reflect on the following code:
// Example 2(a): You cant write an ES // in a typedef. // void f() throw(A,B); // syntax error typedef void (*PF)() throw(A,B); // cant get here PF pf = f;
The throw-specification on the typedef is illegal. C++ doesnt let you write that, so the exception specification is not allowed to participate in the type of a function... at least, not in the context of a typedef, its not. But in other cases, exception specifications do indeed participate in the functions type, such as if you wrote the same function declaration without the typedef:
// Example 2(b): But you can if you omit // the typedef! // void f() throw(A,B); void (*pf)() throw(A,B); // ok pf = f; // ok
Incidentally, you can do this kind of assignment of a pointer to a function as long as the targets exception specification is no more restrictive than the sources:
// Example 2(c): Also kosher, low-carb, // and fat-free. // void f() throw(A,B); void (*pf)() throw(A,B,C); // ok pf = f; // ok, less // restrictive
Exception specifications also participate in a virtual functions type when you try to override it:
// Example 2(d): And the ES in the // signature does matter if its a // virtual function. // class C { virtual void f() throw(A,B); // same ES }; class D : C { void f(); // error, now the ES matters };
So the first issue with exception specifications as they exist in todays C++ is that theyre really a shadow type system that plays by different rules than the rest of the type system.
Issue the Second: (Mis)understandings
The second issue has to do with knowing what youre getting. As many notable persons, including the authors of the Boost exception specification rationale [2], have put it, programmers tend to use exception specifications as though they behaved the way the programmer would like, instead of the way they actually do behave. (For a brief mention of this, with longer related discussion about whether exception safety is worth it, see [3].)
Heres what many people think that exception specifications do:
- Guarantee that functions will only throw listed exceptions (possibly none).
- Enable compiler optimizations based on the knowledge that only listed exceptions (possibly none) will be thrown.
The above expectations are, again, deceptively close to being correct. Consider again the code in Example 1(b):
// Example 1(b) reprise, and two // potential white lies: // int Gunc() throw(); // will throw nothing (?) int Hunc() throw(A,B); // can only throw A or B (?)
Are the comments correct? Not quite. Gunc() may indeed throw something, and Hunc() may well throw something other than A or B! The compiler just guarantees to beat them senseless if they do... oh, and to beat your program senseless too, most of the time.
Because Gunc() or Hunc() could indeed throw something they promised not to, not only cant the compiler assume it wont happen, but the compiler is responsible for being the policeman with the billy club who checks to make sure such a bad thing doesnt happen undetected. If it does happen, then the compiler must invoke the unexpected() function. Most of the time, that will terminate your program. Why? Because there are only two ways out of unexpected(), neither of which is a normal return. You can pick your poison:
- Throw instead an exception that the exception specification does allow. If so, the exception propagation continues as it would normally have. But remember that the unexpected() handler is global there is only one for the whole program. A global handler is highly unlikely to be smart enough to Do the Right Thing for any given particular case, and the result is to go to terminate(), go directly to terminate(), do not pass catch, do not collect $200.
- Throw instead (or rethrow) an exception that the exception specification (still) doesnt allow. If the original function allowed a bad_exception type in its exception specification, okay, then its a bad_exception that will now get propagated. But if not, then go to terminate(), go directly to terminate()....
Because violated exception specifications end up terminating your program the vast majority of the time, I think its legitimate to call that beat[ing] your program senseless.
Above, we saw two bullets stating what many people think that exception specifications do. Here is an edited statement that more accurately portrays what they actually do do:
- Enforce at run time that functions will only throw listed exceptions (possibly none).
- Enable or prevent compiler optimizations based on having to check whether listed exceptions are indeed being thrown.
To see what a compiler has to do, consider the following code, which provides a body for one of our sample functions, Hunc():
// Example 3(a) // int Hunc() throw(A,B) { return Junc(); }
Functionally, the compiler must generate code like the following, and its typically just as costly at run time as if youd hand-written it yourself (though less typing because the compiler generates it for you):
// Example 3(b): A compilers massaged // version of Example 3(a) // int Hunc() try { return Junc(); } catch( A ) { throw; } catch( B ) { throw; } catch( ... ) { std::unexpected(); // will not return! but } // might throw an A or a B if youre lucky
Here we can see more clearly why, rather than letting the compiler make optimizations by assuming only certain exceptions will be thrown, its exactly the reverse: the compiler has to do more work to enforce at run time that only those exceptions are indeed thrown.
The Scoop on Exception Specifications
Besides the overhead for generating the try/catch blocks shown above, which might be minor on efficient compilers, there are at least two other ways that exception specifications can commonly cost you in run-time performance. First, some compilers will automatically refuse to inline a function having an exception specification, just as they can apply other heuristics such as refusing to inline functions that have more than a certain number of nested statements or that contain any kind of loop construct. This is similar to how some compilers apply similar heuristics to disable inlining, such as refusing to inline functions that have more than a certain number of nested statements or that contain any kind of loop construct. Second, some compilers dont optimize exception-related knowledge well at all and will add the above-shown try/catch blocks even when the function body provably cant throw.
Moving beyond run-time performance, exception specifications can cost you programmer time because they increase coupling. For example, removing a type from the base class virtual functions exception specification is a quick and easy way to break lots of derived classes in one swell foop (if youre looking for a way). Try it on a Friday afternoon checkin, and start a pool to guess the number of angry emails that will be waiting for you in your inbox on Monday morning.
So heres what seems to be the best advice we as a community have learned as of today:
Moral #1: Never write an exception specification.
Moral #2: Except possibly an empty one, but if I were you Id avoid even that.
Boosts experience is that a throws-nothing specification on a non-inline function is the only place where an exception specification May have some benefit with some compilers [emphasis mine]. Thats a rather underwhelming statement in its own right, but a useful consideration if you have to write portable code that will be used on more than one compiler platform.
Its actually even a bit worse than that in practice, because it turns out that popular implementations vary in how they actually handle exception specifications. At least one popular C++ compiler (Microsofts, up to version 7.x) parses exception specifications but does not actually enforce them, reducing the exception specifications to glorified comments. But, on the other hand, there are legal optimizations a compiler can perform outside a function, and which the Microsoft 7.x compiler does perform, that rely on the ES enforcement being done inside each function the idea is that if the function did try to throw something it shouldnt the internal handler would stop the program and control would never return to the caller. So since control did return to the caller, the calling code can assume nothing was thrown and do things like eliminate external try/catch blocks. So on that compiler, because the checking is not done but the legal optimization that relies on it is done, the meaning of throw() changes from the standard check me on this, stop me if I inadvertently throw to a trust me on this, assume Ill never throw and optimize away. So beware: if you do choose to use even an empty throw-specification, read your compilers documentation and check to see what it will really do with it. You might just be surprised. Be aware, drive with care.
Summary
While mentioning this material as part of a broader talk at the ACCU conference this past spring, I asked how many of the about 100 people in the room each time had used exception specifications. About half put up their hands. Then a wag at the back said (quite correctly) that I should also ask how many of those people later took the exception specifications back out again afterwards, so I asked; about the same number of hands went up. This is telling. Boost went through the same experience, and thats why their coding policy on writing exception specifications pretty much boils down to dont do that [2].
True, many well-intentioned people wanted exception specifications in the language, so thats why we have them. This reminds me of a cute poem that I first encountered about 15 years ago as it circulated in midwinter holiday emails. Set to the cadence of Twas the Night Before Christmas, these days its variously titled Twas the Night Before Implementation or Twas the Night Before Crisis. It tells of a master programmer who slaves away late at night in the holiday season to meet user deadlines and performs multiple miracles to pull out a functioning system that perfectly implements the requirements... only to experience a final metaphorical kick in the teeth as the last four lines of the ditty report:
The system was finished, the tests were concluded, The users last changes were even included. And the users exclaimed, with a snarl and a taunt, Its just what we asked for, but not what we want! [4]
The thought resonates as we finish considering our current experience with exception specifications. The feature seemed like a good idea at the time, and it is just what some asked for.
But wait, theres more: might the same be said about export? More on that one next time, when we return....
References
[2] Available via <www.gotw.ca/publications/xc++s/boost_es.htm>.
[3] Herb Sutter. Exception Safety and Exception Specifications Are They Worth It? Guru of the Week #82, available at <www.gotw.ca/gotw/082.htm>.
[4] A web search for a snarl and a taunt will get you several variations on this poem. Enjoy! Alas, the original author of the poem is unknown (to me). If anyone has information about the original, or at least earliest known, source of this poem, please send me mail.
Herb Sutter (<www.gotw.ca>) is secretary of the ISO/ANSI C++ standards committee, author of the acclaimed books Exceptional C++ and More Exceptional C++, and one of the instructors of The C++ Seminar (<www.gotw.ca/cpp_seminar>). In addition to his independent writing and consulting, he is also C++ community liaison for Microsoft.