C++ Exceptions
Volume Number: 11
Issue Number: 10
Column Tag: C++ Workshop
Try C++ Exception Handling
How to make use of the C++ exception standard in your C++ code
By Kent Sandvik
Note: Source code files accompanying article are located on MacTech CD-ROM or
source code disks.
Exception handling is a feature of the ANSI C++ specification first formally proposed
back in 1990, but only recently adopted by compilers on the Macintosh. This has
changed with compiler level support for exceptions in CodeWarrior 6, and the promise
that Symantec will provide this feature in their C++ compiler soon.
For developers used to working with class libraries, exception handling is
nothing new. MacApp, TCL, PowerPlant all have provided macros to implement this
feature with setjump/longjmp calls. However, there are still a number of manual
steps for the programmer because this technique can interact with compiler
optimizations. Supporting exceptions directly in the compiler, which has the ability
to make sure that variables and stack information, including C++ objects, are valid
between exception cases means that the framework and developers don’t need to worry
about these runtime issues. In addition, this means that C++ code is more portable
across computer environments.
Most of the C++ literature dealing with exception handling tends to be academic
and doesn’t show how C++ exception handling is used in real life. This article will get
you started if you want to add standard C++ exception handling into your current
project. I will describe what exception handling is, how it works and how to
implement exception handling using the proposed ANSI C++ syntax. We will also
discuss some pitfalls and design issues.
Error Handling and Exceptions
The generalized model for exception handling is to have, within a part of a program,
separate execution paths for normal execution and error recovery. When an error
(exception) is detected, the program can ‘throw’ it to the error recovery code
(exception handler). The recovery code may then attempt to recover from the error
and resume normal program execution, or do some clean up and pass control to the next
handler. As noted above, errors are called exceptions, and the error recovery code is
called an exception handler.
It is important to note that exception handling is just one of many ways to deal
with error cases. The following are some other error reporting techniques used by
software engineers:
• Return an error status from the function (OSErr)
• Global error variable (errno in UNIX environments, QDErr and similar low mem
globals on the Mac side, fortunately we have functions to return these errors
nowadays)
• Invocation of a special, user-defined error function, or callback function
• Jump to a previous point on the stack via setjmp/longjmp
• Use of assertions to test a state, signal if assertion failed
Some examples of error situations are: truly fatal errors, assertions (we assume
that something is OK, and it is not), algorithmic failures (like the dreadful one-off
with array elements), and alternate return signals (end of file markers arriving with
a file stream for instance). As you can see, these are not all fatal errors or errors at
all, but they are exceptional cases which can be handled using exception handling.
The beauty of exception handling lies in not only how it relieves the programmer
from handling the exceptional situations explicitly, but also allows increased
flexibility in dealing with exceptions. For instance, low-level library code needs to
signal information about an exceptional condition to the user interface through
intervening library code. Let’s say that a network connection is needed and certain
C++ objects need to be created to manage it. One part of the code will try to create the
objects. If the object creation leads to an unexpected state (suddenly can’t make the
connection), the network object could throw an exception back to the code that tries to
use it, and at the same time unwind any partially or fully created objects and other
variables on the stack.
Should exception handling should be used to handle all these cases, or should it be
used selectively?
Several facts speak in favor of using it liberally. The code is cleaner if the error
handling is factored out from the normal flow of execution. Error handling code is
reused more often, and the actual function blocks would spend less time dealing with
error situations across the board, because the task is delegated to specifically designed
exception catch blocks that deal uniformly with error situations. Such catch blocks
could provide a uniform way of informing end users about error conditions.
On the other hand, because some runtime environments impose a higher runtime
overhead on exception processing than normal execution, exception handling should not
be used casually as a way to signal information from one part of the call chain to
another, or as a quick and dirty way to implement message passing inside an
application.
[Some programmers may disagree with this latter assertion. For instance, it is
often useful to use an exception as a means of loop termination when it is easier to
detect the termination condition and recover than it is to predict it - such as throwing
an EOF exception when trying to read past the end of a file. This has the same effect as
a break, but can be done several levels deep in the call chain (as you can with longjmp,
but more intuitively). In general, I tend to use a 10:1 rule when using an exception
within the normal - if the normal case is executed at least 10 times for each time an
exception is thrown then an exception is okay. - sgs]
Why Compiler Support for Exceptions is Better
MacApp, TCL and similar frameworks have implemented specific macros and data
structures that provide a way to signal exceptions, provide information about the
exception, and indicate that a handler that will catch the exception. In most cases these
techniques are built around the ANSI C setjmp and longjmp functions. setjmp saves the
current program environment inside a jmp_buf data structure. longjmp will let you
jump back to the point saved with setjmp. Think of it as a very smart goto that keeps
track of the environment setting.
Macro implementations of exceptions work fine, generally, but there are some
known problems. Smart compilers like to optimize variables, stuff them into
registers or eliminate them. If this happens, the longjmp back to the earlier point
might lead to a situation where things are not the same after all. The solution is to
declare variable volatile. This is a way to tell the compiler to keep its hands off the
variable. Unfortunately, not all compilers have a working volatile keyword. What
MacApp had to do in this case was to create a VOLATILE macro. All this macro does is
take the address of the variable, which ensures that the compiler (Cfront in this case)
will not place this variable into a register. Modern compilers are still pretty keen on
optimizing away variables if they have a chance; hopefully such compilers have a
working volatile keyword.
Another issue concerns threads and maintaining a linked list of exception
handlers. In many cases we want to have a list of exceptions that are valid (for
instance the FailInfo implementation of MacApp). If the application spins off threads,
and one of these threads throw an exception, there’s a chance we want to roll back to an
earlier state. In such cases we want to yank out the exception structure from the
linked list. Now, if we don’t keep track of this case with the thread system (for
instance, by creating our own thread dispatcher that will know of such data structures
and keep the linked list intact), we might yank out the exception frame from the middle
of the list and suddenly the linked list is no longer linked.
Also, if we want to roll back to a known state, and this means that there are
objects created that should be destructed, it is really up to the programmer to know
about this situation.
This is where a compiler level implementation of exception handling is better.
Compilers know about what variables should be kept volatile, what data structures are
allocated and when these should be destructed and so on. Dealing with threads case is
still a tricky one, though. This is a good example where the platform architecture and
the language syntax/semantics are not harmonized. C++ does not go far enough to take
care of issues related to threads, and it is the the programmer’s responsibility to be
alert about this and similar cases.
C++ exception handling ensures that all class instances will be properly
de-allocated off the runtime stack or from the heap. This means that we can now take
actions in constructors that can fail because it is possible to return an error result by
throwing an exception. If a constructor fails, the de-allocation code is called by the
compiler, so that we don’t need to worry about memory leaks and non-allocated objects
on the stack. For instance, a constructor could try to create files, and if it fails, the
destructor will remove them. In other words, constructors are now far more
productive.
Needless to say, C++ exception handling requires runtime support. For normal
functions the compiler knows where in a program a function is called, and knows
where to pick up execution when the function returns. For exception handling the
compiler does not know for a particular throw expression what function the catch
block resides in, and where execution will resume from after the exception has been
handled. These decisions won’t happen until runtime, so the compiler will leave
information around for the proper decisions to take place (generates data structures to
hold this information needed during runtime). This means additional overhead in the
application execution time, stack size and so on.
Exception Basics, Try and Catch
Someone somewhere will signal that they have an exceptional situation. When an
exception occurs, an exception handler is triggered on a higher level, and depending on
the situation the handler may do any number of things. It is up to the programmer to
indicate where exceptions may occur and should be handled. In the following code, the
errors are handled in the block marked with the keyword “try”:
DoSomething()
{
// I’m in trouble
throw exception;
}
try {
DoSomething(); // This will throw an exception, and the catch
// below will catch it.
};
catch (exception)
{
// Ooops, what to do?
}
So what should we do inside the catch block? We might change something and
retry the action, return normally (as nothing really happened), rethrow the same
exception with modifications and assume that a higher level catch block will help out,
throw a different exception, continue execution to the next statement, or just
terminate the program.
When we design applications it is important to define what each layer of the
application (from the low level utility functions all the way to the end user level)
should do with an exception when exceptions are triggered. Various libraries, such as
the new ANSI C++ standard library, include exceptions thrown from various class
member functions. It is important that the application will catch any relevant
exceptions thrown - assuming that the exceptions are documented, of course. We’ll
discuss design issues in more detail later.
C++ Exception Coding, Exception Classes
Here’s a more complex example. We implement exceptions that will throw objects
instantiated from classes we’ve defined. Let’s investigate each case one at a time:
try
{
aFoo = new Foo;
aFoo->DoASillyThing(); //This will throw an exception
}
Here we will do an initialization of a Foo class, and then call a member function
that will trigger an unexpected situation (that was hard coded). The try block will tell