A Pragmatic Look at Exception Specifications This article appeared in C/C++ Users Journal, 20(7), July 2002. As we consider work now underway on the new C++ standard, C++0x, it’s a good time to take stock of what we’re 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 lion’s share of the print because there’s 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 they’re even there (not always a bad thing). That’s why you’ve 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, let’s 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)

//

int Func(); // can throw anything 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 it’s 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)

//

int Gunc() throw(); // will throw nothing



int Hunc() throw(A,B); // can only throw A or B In these cases, the function’s 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. We’ll 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++ isn’t 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 that’s 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 don’t participate in a function’s type. b) Except when they do. Consider first an example of when exception specifications don’t participate in a function’s type. Reflect on the following code: // Example 2(a): You can’t write an ES

// in a typedef.

//

void f() throw(A,B);



typedef void (*PF)() throw(A,B); // syntax error



PF pf = f; // can’t get here The throw-specification on the typedef is illegal. C++ doesn’t let you write that, and so the exception specification is not allowed to participate in the type of a function… at least, not in the context of a typedef, it’s not. But in other cases, exception specifications do indeed participate in the function’s 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 target’s exception specification is no more restrictive than the source’s: // 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 function’s type when you try to override it: // Example 2(d): And the ES in the signature

// does matter if it’s 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 today’s C++ is that they’re 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 you’re 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].) Here’s 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 can’t the compiler assume it won’t happen, but the compiler is responsible for being the policeman with the billy club who checks to make sure such a bad thing doesn’t 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: a) 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. b) Throw instead (or rethrow) an exception that the exception specification (still) doesn’t allow. If the original function allowed a bad_exception type in its exception specification, okay, then it’s 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 it’s 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: Guarantee Enforce at runtime that functions will only throw listed exceptions (possibly none). Enable or prevent compiler optimizations based on the knowledge that only listed exceptions (possibly none) will be thrown 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 it’s typically just as costly at runtime as if you’d hand-written it yourself (though less typing because the compiler generates it for you): // Example 3(b): A compiler’s 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 you’re lucky Here we can see more clearly why, rather than letting the compiler make optimizations by assuming only certain exceptions will be thrown, it’s exactly the reverse: the compiler has to do more work to enforce at runtime 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 runtime 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. Second, some compilers don’t optimize exception-related knowledge well at all, and will add the above-shown try/catch blocks even when the function body provably can’t throw. Moving beyond runtime performance, exception specifications can cost you programmer time because they increase coupling. For example, removing a type from the base class virtual function’s exception specification is a quick and easy way to break lots of derived classes in one swell foop (if you’re 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 here’s 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 I’d avoid even that. Boost’s 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]. That’s 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. It’s 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 (Microsoft’s, 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 shouldn’t 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 I’ll never throw and optimize away.” So beware: If you do choose to use even an empty throw-specification, read your compiler’s 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. The world-class library designers at Boost went through the same experience, and that’s why their coding policy on writing exception specifications pretty much boils down to “don’t do that.” [2] True, many well-intentioned people wanted exception specifications in the language, and so that’s 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 it’s 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,

“It’s 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, there’s more: Might the same be said about export? More on that one next time, when we return… Notes [1] See www.edg.com. [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). [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.