Exception Safety and Exception Specifications: Are They Worth It?
Difficulty: 8 / 10
Is it worth the effort to write exception-safe code? Are exception specifications worthwhile? It may surprise you that these are still disputed and debated points, and ones where even experts may sometimes disagree.
Problem
JG Questions
1. Recap: Briefly define the Abrahams exception safety guarantees (basic, strong, and nothrow).
2. What happens when an exception specification is violated? Why? Discuss the basic rationale for this C++ feature.
Guru Questions
3. When is it worth it to write code that meets:
a) the basic guarantee?
b) the strong guarantee?
c) the nothrow guarantee?
4. When is it worth it to write exception specifications on functions? Why would you choose to write one, or why not?
Solution
JG Questions
1. Recap: Briefly define the Abrahams exception safety guarantees (basic, strong, and nothrow).
The basic guarantee is that failed operations may alter program state, but no leaks occur and affected objects/modules are still destructible and usable, in a consistent (but not necessarily predictable) state.
The strong guarantee involves transactional commit/rollback semantics: failed operations guarantee program state is unchanged with respect to the objects operated upon. This means no side effects that affect the objects, including the validity or contents of related helper objects such as iterators pointing into containers being manipulated.
The nothrow guarantee means that failed operations will not happen. The operation will not throw an exception.
2. What happens when an exception specification is violated? Why? Discuss the basic rationale for this C++ feature.
The idea of exception specifications is to do a run-time check that guarantees that only exceptions of certain types will be emitted from a function (or that none will be emitted at all). For example, the following function's exception specification guarantees that f() will emit only exceptions of type A or B:
int f() throw( A, B );
If an exception would be emitted that's not on the "invited-guests" list, the function unexpected() will be called. For example:
int f() throw( A, B )
{
throw C(); // will call unexpected()
}
You can register your own handler for the unexpected-exception case by using the standard set_unexpected() function. Your replacement handler must take no parameters and it must have a void return type. For example:
void MyUnexpectedHandler() { /*...*/ }
std::set_unexpected( &MyUnexpectedHandler );
The remaining question is, what can your unexpected handler do? The one thing it can't do is return via a usual function return. There are two things it may do:
1. It could decide to translate the exception into something that's allowed by that exception-specification, by throwing its own exception that does satisfy the exception-specification list that caused it to be called, and then stack unwinding continues from where it left off.
2. It could call terminate(). (The terminate() function can also be replaced, but must always end the program.)
Guru Questions
3. When is it worth it to write code that meets:
a) the basic guarantee?
b) the strong guarantee?
c) the nothrow guarantee?
It is always worth it to write code that meets at least one of these guarantees. There are several good reasons:
1. Exceptions happen. (To paraphrase a popular saying.)
They just do. The standard library emits them. The language emits them. We have to code for them. Fortunately, it's not that big a deal, because we now know how to do it. It does require adopting a few habits, however, and following them diligently -- but then so did learning to program with error codes.
The big thorny problem is, as it ever was, the general issue of error handling. The detail of how to report errors, using return codes or exceptions, is almost entirely a syntactic detail where the main differences are in the semantics of how the reporting is done, and so each approach requires its own style.
2. Writing exception-safe code is good for you.
Exception-safe code and good code go hand in hand. The same techniques that have been popularized to help us write exception-safe code are, pretty much without exception, things we usually ought to be doing anyway. That is, exception-safety techniques are good for your code in and of themselves, even if exception safety weren't a consideration.
To see this in action, consider the major techniques I and others have written about to make exception safety easier:
Use “resource acquisition is initialization” (RAII) to manage resource ownership. Using resource-owning objects like Lock classes and auto_ptrs is just a good idea in general. It should come as no surprise that among their many benefits we should also find "exception safety." How many times have you seen a function (here we're talking about someone else's function, of course, not something you wrote!) where one of the code branches that leads to an early return fails to do some cleanup, because cleanup wasn't being managed automatically using RAII?
Use “do all the work off to the side, then commit using nonthrowing operations only” to avoid changing internal state until you’re sure the whole operation will succeed. Such transactional programming is clearer, cleaner, and safer even with error codes. How many times have you seen a function (and naturally here again we're talking about someone else's function, of course, not something you wrote!) where one of the code branches that leads to an early return fails to preserve the object's state, because some fiddling with internal state had already happened before a later operation failed?
Prefer “one class (or function), one responsibility.” Functions that have multiple effects, such as the Stack::Pop() and EvaluateSalaryAndReturnName() functions described in Items 10 and 18 of Exceptional C++ [1], are difficult to make strongly exception-safe. Many exception safety problems can be made much simpler, or eliminated without conscious thought, simply by following the "one function, one responsibility" guideline. And that guideline long predates our knowledge that it happens to also apply to exception safety; it's just a good idea in and of itself.
Doing these things is just plain good for you.
Having said that, then, which guarantee should we use when? In brief, here's the guideline followed by the C++ standard library, and one that you can profitably apply to your own code:
Guideline:
A function should always support the strictest guarantee that it can support without penalizing users who don't need it.
So if your function can support the nothrow guarantee without penalizing some users, it should do so. Note that a handful of key functions, such as destructors and deallocation functions, simply must be nothrow-safe operations because otherwise it's impossible to reliably and safely perform cleanup.
Otherwise, if your function can support the strong guarantee without penalizing some users, it should do so. Note that vector::insert() is an example of a function that does not support the strong guarantee in general because doing so would force us to make a full copy of the vector's contents every time we insert an element, and not all programs care so much about the strong guarantee that they're willing to incur that much overhead. (Those programs that do can "wrap" vector::insert() with the strong guarantee themselves, trivially: take a copy of the vector, perform the insert on the copy, and once it's successful swap() with the original vector and you're done.)
Otherwise, your function should support the basic guarantee.
For more information about some of the above concepts, such as what a nonthrowing swap() is all about or why destructors should never emit exceptions, see further reading in Exceptional C++ [1] and More Exceptional C++ [2].
4. When is it worth it to write exception specifications on functions? Why would you choose to write one, or why not?
In brief, don't bother. Even experts don't bother.
Slightly less briefly, the major issues are:
Exception specifications can cause surprising performance hits, for example if the compiler turns off inlining for functions with exception specifications.
A runtime unexpected() error is not always what you want to have happen for the kinds of mistakes that exception specifications are meant to catch.
You generally can't write useful exception specifications for function templates anyway because you generally can't tell what the types they operate on might throw.
For more, see for example the Boost exception specification rationale available via http://www.gotw.ca/publications/xc++s/boost_es.htm (it summarizes to "Don't!").
References
[1] H. Sutter. Exceptional C++ (Addison-Wesley, 2000).
[2] H. Sutter. More Exceptional C++ (Addison-Wesley, 2002).