Dr. Greg Colvin, a.k.a. The Macro Meister, represents Information Management Research on the ANSI/ISO committee now standardizing C++. Greg is on the internet as gregor@netcom.com.
Exceptions in C++
An exception is any condition that prevents code from proceeding normally, such as calling a function with invalid arguments, exhausting memory or file space, failing to acquire a database lock, failing to complete a communication, and so on. Exception handling in C code is typically done in four different ways:
These techniques remain useful and valid in C++, but longjmp presents difficulties.
- returning an error value, e.g. NULL
- setting a global error code, e.g. errno
- calling an installed function, e.g. signal
- uonditionally transferring control, e.g. goto or longjmp
To use longjmp you first call setjmp(jmp_buf) to save the current execution context. To handle an exception you can call longjmp(jmp_buf, int) to restore the saved context. For example:
jmp_buf handler; void fun() { ..... if (bad) longjmp(handler, -1); } void attempt() { ..... fun(); ..... } void f() { int error = setjmp(handler); if (!error) attempt(); else report(error); }When setjmp(handler) is called it returns zero. When longjmp(handler, -1) is called it causes setjmp to return -1, having skipped over the intervening call attempt(). For C++ the problem is that any auto objects constructed by attempt() or fun() will not be destroyed, as they would be by a normal function return.Rather than change the semantics of longjmp, which are already given in Standard C, the C++ language provides its own exception handling facility:
void fun() { ..... if (bad) throw -1; } void attempt() { ..... fun(); ..... } void f() { try { attempt(); } catch (int error) { report(error); } }The throw expression transfers control to the nearest corresponding catch clause, having called the destructors for all intervening auto objects. This facility is much more flexible and easy to use than the longjmp facility, allowing for multiple catch clauses which can accept any type of object, with no need for the throw expression to know which handler to invoke.Unfortunately, not all C++ compilers support exception handling, which makes portability a problem. Since I wanted to use exception handling in code that had to be widely portable, I chose to emulate a subset of C++ exception handling. My emulation is based on the classes Unwindable and Handler, and the macros try, catch, throw, and UNWINDABLE. Since we are doing without language support there are some restrictions:
- you must explicitly make objects unwindable (properly destroyed when they go out of scope) by deriving from Unwindable and putting the UNWINDABLE macro at the start of all Unwindable constructors;
- you should declare static Unwindable objects only at file scope;
- you must have one and only one catch clause for each try block;
- you can have only one try block in any scope;
- you can throw only non-zero integers.
How the Code Works
So how does this emulation work? Listing 1 shows the header file unwind.h that you include when you want to use his package. Listing 2 shows the file unwind.cpp, which contains the executable code. The package defines several macros:1. The try macro calls setjmp to save the current execution context on a stack of exception Handlers.
2. The UNWINDABLE macro in the constructor for Unwindable objects pushes constructed objects onto a stack of objects for the current exception Handler.
3. The throw macro pops and destroys all Unwindable objects on the stack for the current handler, pops the stack of exception handlers, and calls longjmp to restore the execution context of the current Handler.
4. The catch macro makes a local copy of the thrown integer and continues execution.
Implementing this emulation takes very little code, but phase 2 is a bit tricky. When an exception is thrown the code has to ensure that incompletely constructed objects are not destroyed, that objects allocated by operator new are not destroyed, and that auto objects are destroyed only once.
The UNWINDABLE macro declares a Pusher object whose destructor pushes the now constructed Unwindable. New objects are detected by having Unwindable::operator new keep a stack of objects under construction by operator new. As each object 0 is pushed, the code checks whether 0 is a subobject of the top newbie. If so, the code pops 0 off the stack of newbies when it is the complete object on the stack. If not, it pops any subobjects of 0 off of, and 0 itself onto, the stack for the current handler. The stacks are maintained via link fields in the Unwindable and Handler objects.
Note that the within function for detecting subobjects is not strictly conforming. Some hardware does not support comparing pointers which do not point into the same object. However, I have seen this function work on Intel and Macintosh personal computers, as well as Apollo, HP, IBM, and Sun workstations.
I have found exception handling to be an invaluable aid to building reliable applications. Although incomplete, my emulation has provided adequate functionality at reasonably low cost. Pete Becker at Borland has used similar techniques to port OWL exception handling from the Borland C++ compiler, which supports exceptions, to the Symantec compiler, which does not. If you need portable exception handling, emulation has proven to be a viable approach.