Error and Exception Handling
References
The following paper is a good introduction to some of the issues of writing robust generic components:
D. Abrahams: ``Exception Safety in Generic Components'', originally published in M. Jazayeri, R. Loos, D. Musser (eds.): Generic Programming, Proc. of a Dagstuhl Seminar, Lecture Notes on Computer Science. Volume. 1766
Guidelines
When should I use exceptions?
The simple answer is: ``whenever the semantic and performance characteristics of exceptions are appropriate.''
An oft-cited guideline is to ask yourself the question ``is this an exceptional (or unexpected) situation?'' This guideline has an attractive ring to it but is usually a mistake. The problem is that one person's ``exceptional'' is another's ``expected'': when you look at the terms carefully, the distinction evaporates and you're left with no guideline. After all, if you check for an error condition, then in some sense you expect it to happen, or the check is wasted code.
A more appropriate question to ask is: ``do we want stack unwinding here?'' Because actually handling an exception is likely to be significantly slower than executing mainline code, you should also ask: ``Can I afford stack unwinding here?'' For example, a desktop application performing a long computation might periodically check to see whether the user had pressed a cancel button. Throwing an exception could allow the operation to be canceled gracefully. On the other hand, it would probably be inappropriate to throw and handle exceptions in the inner loop of this computation because that could have a significant performance impact. The guideline mentioned above has a grain of truth in it: in time-critical code, throwing an exception should be the exception, not the rule.
How should I design my exception classes?
- Derive your exception class from
std::exception
. Except in *very* rare circumstances where you can't afford the cost of a virtual table,std::exception
makes a reasonable exception base class, and when used universally, allows programmers to catch "everything" without resorting tocatch(...)
. For more aboutcatch(...)
, see below. -
Use virtual inheritance. This
insight is due to Andrew Koenig. Using virtual inheritance
from your exception's base class(es) prevents ambiguity
problems at the catch-site in case someone throws an
exception derived from multiple bases which have a base
class in common:
#include <iostream> struct my_exc1 : std::exception { char const* what() const throw(); }; struct my_exc2 : std::exception { char const* what() const throw(); }; struct your_exc3 : my_exc1, my_exc2 {}; int main() { try { throw your_exc3(); } catch(std::exception const& e) {} catch(...) { std::cout << "whoops!" << std::endl; } }
The program above prints"whoops"
because the C++ runtime can't resolve whichexception
instance to match in the first catch clause. -
Don't embed a std::string object or
any other data member or base class whose copy constructor
could throw an exception. That could lead directly to
std::terminate() at the throw point. Similarly, it's a bad
idea to use a base or member whose ordinary constructor(s)
might throw, because, though not necessarily fatal to your
program, you may report a different exception than intended
from a throw-expression that includes construction
such as:
throw some_exception();
There are various ways to avoid copying string objects when exceptions are copied, including embedding a fixed-length buffer in the exception object, or managing strings via reference-counting. However, consider the next point before pursuing either of these approaches.
- Format the
what()
message on demand, if you feel you really must format the message. Formatting an exception error message is typically a memory-intensive operation that could potentially throw an exception. This is an operation best delayed until after stack unwinding has occurred, and presumably, released some resources. It's a good idea in this case to protect yourwhat()
function with acatch(...)
block so that you have a fallback in case the formatting code throws - Don't worry too much about the
what()
message. It's nice to have a message that a programmer stands a chance of figuring out, but you're very unlikely to be able to compose a relevant and user-comprehensible error message at the point an exception is thrown. Certainly, internationalization is beyond the scope of the exception class author. Peter Dimov makes an excellent argument that the proper use of awhat()
string is to serve as a key into a table of error message formatters. Now if only we could get standardizedwhat()
strings for exceptions thrown by the standard library... - Expose relevant information about the cause of
the error in your exception class' public interface.
A fixation on the
what()
message is likely to mean that you have neglected to expose information someone might need, in order to make it a coherent message for users. For example, if your exception reports a numeric range error, it's important to have the actual numbers involved available as numbers in the exception class' public interface where error reporting code can do something intelligent with them. If you only expose a textual representation of those numbers in thewhat()
string, you will make life very difficult for programmers who need to do something more (e.g. subtraction) with them than dumb output. - Make your exception class immune to double-destruction if possible. Unfortunately, several popular compilers occasionally cause exception objects to be destroyed twice. If you can arrange for that to be harmless (e.g. by zeroing deleted pointers) your code will be more robust.
What About Programmer Errors?
As a developer, if I have violated a precondition of a
library I'm using, I don't want stack unwinding. What I want is
a core dump or the equivalent - a way to inspect the state of
the program at the exact point where the problem was detected.
That usually means assert()
or something like
it.
Sometimes it is necessary to have resilient APIs which can stand up to nearly any kind of client abuse, but there is usually a significant cost to this approach. For example, it usually requires that each object used by a client be tracked so that it can be checked for validity. If you need that sort of protection, it can usually be provided as a layer on top of a simpler API. Beware half-measures, though. An API that promises resilience against some, but not all abuse is an invitation to disaster. Clients will begin to rely on the protection and their expectations will grow to cover unprotected parts of the interface.
Note for Windows developers: unfortunately,
the native exception-handling used by most Windows compilers
actually throws an exception when you use
assert()
. Actually, this is true of other
programmer errors such as segmentation faults and
divide-by-zero errors. One problem with this is that if you use
JIT (Just In Time) debugging, there will be collateral
exception-unwinding before the debugger comes up because
catch(...)
will catch these not-really-C++
exceptions. Fortunately, there is a simple but little-known
workaround, which is to use the following incantation:
extern "C" void straight_to_debugger(unsigned int, EXCEPTION_POINTERS*) { throw; } extern "C" void (*old_translator)(unsigned, EXCEPTION_POINTERS*) = _set_se_translator(straight_to_debugger);
This technique doesn't work if the SEH is raised from within a catch block (or a function called from within a catch block), but it still eliminates the vast majority of JIT-masking problems.
How should I handle exceptions?
Often the best way to deal with exceptions is to not handle them at all. If you can let them pass through your code and allow destructors to handle cleanup, your code will be cleaner.
Avoid catch(...)
when
possible
Unfortunately, operating systems other than
Windows also wind non-C++ "exceptions" (such as thread
cancellation) into the C++ EH machinery, and there is sometimes
no workaround corresponding to the
_set_se_translator
hack described above. The
result is that catch(...)
can have the effect of
making some unexpected system notification at a point where
recovery is impossible to look just like a C++ exception thrown
from a reasonable place, invalidating the usual safe
assumptions that destructors and catch blocks have taken valid
steps to ensure program invariants during unwinding.
I reluctantly concede this point to Hillel Y. Sims, after
many long debates in the newsgroups: until all OSes are
"fixed", if every exception were derived from
std::exception
and everyone substituted
catch(std::exception&)
for
catch(...)
, the world would be a better place.
Sometimes, catch(...)
, is still the most
appropriate pattern, in spite of bad interactions with
OS/platform design choices. If you have no idea what kind of
exception might be thrown and you really must stop
unwinding it's probably still your best bet. One obvious place
where this occurs is at language boundaries.