The Secret Life of C++: Exceptions
Exceptions are wonderful things. They let you report error
conditions in vaguely correct ways. But C++ exceptions have to
live in a highly hostile environment. No virtual machine to coddle
them, they have to leap over large blocks of legacy C code
unharmed. TODO: More flavortext.
For more detail about exception handling, check out .
How Exceptions Work
Exceptions are basically a non-local-goto. They allow a program to
transfer control up the stack, in cases when the code at the
bottom of the stack doesn't know what else to do. They are even
more flexible then this becuase there is a registry system, so
that an exception is thrown up the stack until it hits the first
stack frame where some expressed interest in dealing with it (ie,
a catch () handler).
But this is C++, and nothing is easy. So on our way up the stack,
it is important that we properly destroy our stack frames. In C,
we could just ignore them, becuase there is no such thing as a
destructor. Of course, this simplicity makes it very hard to make
C programs that properly handle non-local gotos. But we are
talking about C++ and complexity.
So the basic way in which we handle exceptions is to walk up the
stack, calling the cleanup code as we go, until we find someone
who wants to handle our exception.
Exceptions are also objects, and the fact that they can use
inheritance is important. They have an object lifecycle, they get
created and destroyed. We will look at this in more detail.
Hello World-ish example.
A Hello Worldish example of exceptions:
Time for our good old list of symbols:
- _ZNSaIcEC1E
- std::allocator<char>::allocator
- _ZNSsC1EPKcRKSaIcE
- string(const char *)
- _ZNSsD1Ev
- ~string()
- __cxa_allocate_exception
- Allocate memory for an exception.
Generally on the heap. Has access to a last-resort piece of memory
for this purpose, so we can throw out of memory exceptions.
- __cxa_throw
- External interface to throw in the C++ support
library. Takes three arguments: an exception object, a typeinfo
for that object, and a pointer to the destructor to call when we
are done with that object.
- _Unwind_RaiseException
- Function called by __cxa_throw.
- _Unwind_Resume
- Resume the unwind process, called at the end
of cleanup code that didn't return to the normal thread of
execution (ie, not a catch).
- __cxa_begin_catch
- Keeps track of which exceptions are being
caught in which order, pushes this exceptoin on the stack of
exceptions that are being handled.
- __cxa_end_catch
- Take the exception we are processing off
the stack and free it. When it returns, we should be in our normal
execution thread.
Unwinding the Stack
How does unwinding the stack really work? It happens in two passes.
On the first pass (Phase One), we walk up the stack until we find
an exception handler that wants to handle our exception. it is
even possible to find a handler up the stack that tells us to
ignore the exception. I'm told this functionality is used in
Common Lisp implementations.
The second pass (Phase Two) walks up the stack, executing the
cleanup code, until we get to the frame which is going to do the
catch.
There are two specific methods:
SjLj stack unwinding
In SjLj (Setjmp, Longjmp) stack unwinding, we do a setjmp-ish call
each time we enter a function. As we go up the stack, we just
longjmp to each setjmp point in succession.
An example of SjLj exceptions:
Dwarf2 stack unwinding
In Dwarf2 stack unwinding we don't have to do any work as long as
there are no exceptions, but complexity is increased. We create a
symbol table similar to debugging symbols that lets us find out
the right places to walk up the stack to.
Forced Unwind versus Regular
There is the concept of a Forced unwind of the stack. A Forced
unwind is not caused by an exception being thrown. A forced unwind
is when the exception handlers on the call stack aren't allowed to
catch an exception, and some other code takes care of knowing when
to stop. Two examples of forced unwind are longjmp() and
pthread_cancel().
Rethrowing
An example of rethrowing a caught exception:
Catching and Throwing a Different Exception
See the above example. Pretty straightforward. unless we want to
throw and catch an exception
while handling an exception.
throw() specification on a function
TODO: There is a new syntax for this, and throw specifications are deprecated.
The throw() specification will cause the unwind Phase One to fail
with unexepected exception.
Passing through code that doesn't know about exceptions
THis works out, partly because code that doesn't know about
exceptions can't have destructors to be called. More succinctly,
longjmp just works in native C code, stack frames can just be
discarded.