Exception handling in C++ offers many advantages over error handling in C. Using the
techniques outlined here, you can implement C++ exceptions in your C code without a
lot of effort. The payback is streamlined debugging that can result in more error-free
code. When your program encounters errors, it jumps to the appropriate
error-handling section, rather than dealing with the error locally. This simplifies
your design and helps you concentrate on the normal flow of control. Centralized error
handling also makes it easier to improve your reporting and feedback mechanisms
incrementally.
I wrote a few little XCMDs in C and after the fifteenth crash of the day, I decided that I'd
better add some error handling. So I looked at Dartmouth XCMDs, but I wasn't
impressed. Each check for an error meant another indentation in the code, and I was
worried about disposing of handles correctly as I passed errors up the call chain. Since
I'd been looking at a lot of C++ lately, I wondered whether I couldn't use part of the
C++ exception-handling mechanism to avoid problems in my code. It worked pretty
well, so I thought I'd share my results.
For part of my solution, I used some Metrowerks macros. Metrowerks has graciously
allowed these helpful exception and debugging source, header, and resource files to be
included on this issue's CD, so you can use them without purchasing its CodeWarrior
CD. The files contain macros that provide convenient tools for implementing exceptions
and debugging signals, as well as an alert resource that can provide information during
debugging.
Although I've used C++ exception handling in my C code with great results, I'd like to
offer you one word of caution before you use them. Realize that C++ is not strictly an
extension of C; as a result, in some cases it's possible that the program may not behave
as you think it should.
All programs must respond to system and subroutine failures somehow. For example,
many Macintosh Toolbox routines return a variable of type OSErr, while others
require that you call Toolbox routines (such as MemError and ResError) to retrieve
the error. If you ignore system and subroutine failures, your program is practically
guaranteed to crash.
Good error handling allows you to cope with many kinds of problems. Your checks can
trigger other code that deals with the problem (for example, by freeing memory).
During debugging, error checking should notify you that something has gone wrong.
And since you can't, unfortunately, catch all the bugs during testing, you must also set
up an error-reporting mechanism to notify your users when something has gone
wrong. In the worst case, your error handling should at least ensure that your
program exits gracefully, without losing or corrupting user data.
The American National Standards Institute (ANSI) has defined a mechanism for C++
compilers that allows code to "throw" exceptions. When the compiler encounters a
throw statement, it jumps to the nearest catch statement. (The "nearest" catch
statement is the one associated with the current try statement, whether it's in the
current routine or farther up the call chain.) The catchstatement can deal with the
error, pass it up the call chain, or both. A throwstatement should appear only within
a try or catch statement or in code called from within a try statement. Listing 1
shows these basic components.
Listing 1. Throwing exceptions
OSErr theErr = noErr;
// Try block.
try {
// Do something.
...
// If error, throw an exception.
if (theErr != noErr)
throw (theErr);
}
// Catch blocks.
catch (OSErr theErr) {
// Do something with the error.
...
}
catch (...) {
// Catch anything else.
...
}
As shown in Listing 1, exceptions are dealt with in catch blocks, which take an
appropriate action depending on the error. For serious errors, this means cleaning up
and terminating the program. For less serious errors, the catch block could continue
without making a fuss, or make changes based on the error and again call the routine
that threw the error; sometimes you might want to throw a more generic error, which
is caught and interpreted in a higher-level catch block. I also recommend using the
Metrowerks signal macros (described later) within your catch blocks to help you
locate errors during debugging.
The three dots in catch (...) are actually in the code; the other such dots
that appear in these listings are ellipses representing code that isn't shown.*
When carefully designed, C++ exception handling in your program can deal with
problems at an appropriate level. As you may already have guessed, this feature is
both powerful and dangerous. The advantage is that you don't have to mess around with
returning errors for every routine or indenting deeply. However, if you allocate
memory, you must be careful to dispose of it at the right time or it will cause a leak.
To add C++ exceptions to your code, you must do the following:
The Metrowerks macros that you'll see in the code that follows make implementing
exception handling much easier than it would be otherwise. I'll talk about them later.
USING C++
To use C++ exceptions, you have to force the use of the C++ compiler. In Metrowerks
CodeWarrior, the easiest way is to select the Activate C++ Compiler checkbox in the
C/C++ Language panel. You should also make sure that the Enable C++ Exceptions
checkbox is selected, because it enables throwing exceptions rather than direct
destruction (one of those weird C++ things). An alternative way to invoke the
compiler is to change the extension on your source code files to ".cp" or by changing
the Target panel preferences; however, the checkbox method is the easiest.
C++ is stricter about automatic parameter conversion than C, so selecting the MPW
Pointer Type Rules checkbox in the C/C++ Language panel avoids a bunch of errors (it
forces the compiler to allow some implicit char* casts). But you'll get errors for
other parameters and return values, so you have to clean them up as indicated by the
compiler. For example, the following is an error message returned by a C++
compiler:
HC2RTF.c line 224 textLen = strlen(textString); Error : cannot convert 'unsigned char *' to 'char *'
To fix this problem, you can change the code to
textLen = strlen((char *) textString)
The CodeWarrior C++ compiler puts special C++ information into function names
Listing 2. Preventing name mangling
#ifdef __cplusplus
extern "C" {
#endif
long FindBreak(char* buffer, short len);
// More declarations here
...
#ifdef __cplusplus
}
#endif
CREATING A TOP-LEVEL EXCEPTION HANDLER IN MAIN
In your main loop or function, you should specify the top-level exception handler. This
should catch serious errors, report them, and exit gracefully. Listing 3 shows the
simplest possible exception handler (which you'll understand better as you read on).
Listing 3. Simple top-level exception handler
pascal void main(XCmdPtr paramPtr)
{
long oldA4 = SetCurrentA4();
try {
CreateFile(paramPtr);
WriteFile(paramPtr);
}
catch (...) {
ReportError("\pSerious error occurred.")
// XCMDs do not have to use ExitToShell.
}
SetA4(oldA4);
}
DEFINING TRY BLOCKS
When you use a try statement, it tells the compiler that the following code might have
exceptions thrown in it. All functions that throw exceptions must be within a try
block, either in the current function or in a calling function. It's pretty easy to set up
try blocks before catch blocks. This is good, because you do have to do it: any throws
that aren't caught will automatically abort the program.
DEFINING CATCH BLOCKS
You should have catch blocks for each error type. So, for example, you might define
catch (OSErr theErr), catch (errStruct errRecord), and catch (Str255
theErr). You should also have a generic catch, catch (...), which doesn't have any
parameters, to catch exceptions of all other types. Although it's better to use typed
catches that handle specific errors, always add at least one generic catch and have it
signal an error with an alert or break to the debugger. This will help you catch
exception mistakes during your debugging and testing phase. Listing 4 shows examples
of these types of catch blocks.
Listing 4. Specific and generic catch blocks
catch (StringPtr errString) {
// If HandleError throws, it will be caught above this catch.
HandleError(errString);
}
catch (OSErr theErr) {
Str255 errString;
ConvertErrToString(theErr, errString);
ReportError(errString);
throw (theErr); // Rethrow to handle error.
}
// Forces the application to quit after the message.
catch (...) {
SignalPStr_("\pUntyped error occurred in prefs.")
ExitToShell();
}
The compiler automatically routes the error to the appropriate catch statement,
depending on the parameter passed to the throw statement. In Listing 4, both the
StringPtr and OSErr types are caught specifically, after which they're reported. The
OSErr catch rethrows the error as well. Any other types of errors are caught by the
generic catch, which calls a signal macro to display a message and then exits the
program.
You can, and often should, continue after catching an error. For example, after a disk
full error, you should allow the user to choose a different volume. Note that the
program will continue after the catch block, rather than in the location where the
exception was thrown.
Many of your low-level routines may call the Macintosh Toolbox or otherwise interact
with the Mac OS. They should throw an exception if there's an error, as shown in
Listing 5.
Listing 5. Throwing exceptions for Macintosh Toolbox errors
void MakeMyResFile(Str32 fileName)
{
CreateResFile(fileName);
// Could also use the Metrowerks ThrowIfResError_ macro.
err = ResError();
if (err <> noErr)
throw (err);
// Continue with execution.
...
}
// Call the function.
MakeThisFile()
{
...
try {
MakeMyResFile(thisFile);
}
catch (OSErr theErr) {
if (theErr == dupFNErr) {
// Do something; file already exists.
...
}
else
throw (theErr); // Rethrow the error.
} // End catch statement.
...
}
So where do you catch these exceptions? Remember, they percolate up the call chain
until they find a catch statement, so you don't have to take care of them in the
immediate calling function (unless you've allocated memory or done other things that
need undoing). When you catch them, you can, and sometimes should, throw the error
again. You can either report errors in mid-level routines or rethrow them up to a
higher-level error reporting mechanism.
In addition to these catch statements, be sure to add a catch statement in circumstances
where you need to do any of the following:
For your own functions, you should throw errors in situations that can cause serious
problems or crash the machine. For instance, if you're providing a function that
accesses a variable-length array that contains 16 members and the caller asks for the
17th member, you can throw a range error. There's no hard-and-fast rule about when
to put the error checking into a function and when to require it before calling -- it
depends on the situation. For example, if you're calling a function inside a tight
graphics loop only and you want speed, you can probably check the parameters
sufficiently in the calling function. However, if you have a utility routine that's called
from several sections of your code, adding error checking will help you remember its
requirements, such as parameters, memory, and other system states, to avoid
problems later on.
Handling exceptions in libraries is tricky because you don't know much about the
calling program. Think carefully about what you should report to the user and what
you should simply return to calling functions.
As your programs become more sophisticated, you can start working around certain
errors -- for example, by using temporary memory when the application's heap is
full. You'll also need to design interactive error reporting, allowing your users to take
action (such as unlocking a locked disk) when they can. Then your application can
continue properly.
The Metrowerks PowerPlant UDebugging and UException files, included on this issue's
CD, provide convenient tools for throwing common exceptions and alerting you during
debugging. To use them, put the folder in your project folder, add the sources and the
"PP DebugAlerts.rsrc" resource file to your project, and include the headers in your
source files.
The UException.h file includes macros that automate common exception conditions.
The UException.cp file includes an abort function. The UDebugging.h file defines some
macros that make locating problems easier by allowing you to specify a signal, a
debugging string displayed when the macro is invoked.
If your project includes an ANSI library you don't need to add
UException.cp. The abort function will conflict.*
SETTING GLOBAL VARIABLES FOR DEBUGGING
You need to set the global variables gDebugThrow and gDebugSignal in UDebugging.h to
specify the debugging actions for throws and signals. By default, they're set to do
nothing at all. Other options include displaying a dialog, dropping into the source-level
debugger, or dropping into the low-level debugger. To activate the macros, be sure to
define Debug_Signal in your precompiled header or UDebugging.h.
The following are the global variable options:
User Break at routine + offset exception code
THE THROW MACROS
UException.h defines several useful macros that automatically perform tests and throw
exceptions if a test failed. It also defines a type, ExceptionCode (a long), and two
standard exceptions, err_AssertFailed ('asrt') and err_NilPointer ('nilP'), which are
treated as type ExceptionCode. Here are the throw macros:
You can use all of the macros within if-else clauses, as they're designed to be
self-contained. For example:
if (err != fnfErr) ThrowIfOSErr_(err);
THE SIGNAL MACROS
UDebugging.h defines macros for raising signals, also known as asserts. These will
stop the execution of the program and report errors. You can use them to check for nil
pointers, out-of-range offsets, excess length, division by zero, and other problems. If
you remove the definition of Debug_Signal, the entire set of macros is converted to
white space and takes no runtime overhead whatsoever.
The macros are defined to check gDebugSignal for the action to take on execution, as
described previously.
The following are the signal macros:
C++ exceptions and these Metrowerks macros make error handling reasonably easy to
add to most programs. With a little thought, you can design a clean structure for
dealing with Mac OS errors and internal errors -- a structure that's easily extensible
to new code. You can avoid stress during testing by adding signal macro calls for
common errors throughout your code. They're much easier to debug than system
crashes. And yes, thank you, my XCMDs are much better now!
RELATED READING
Because C has no objects, when you read these publications, you can ignore all
discussions of object throwing, exception objects, construction, and
destruction.
AVI RAPPOPORT has degrees in medieval studies and library/information studies, so
she feels well qualified to work in the Macintosh software industry. In her job as user
advocate and publications coordinator at Metrowerks, she spent her time documenting
PowerPlant, making conference calls, and frantically trying to check CodeWarrior CDs
before they were burned. Avi now works at StarNine as product manager for messaging
products. She lives in Berkeley, California, with her Mac/Web scripter husband and
their four-year-old son -- all BMUG members.*
Thanks to Greg Dow, Pete Gontier, Tom Lippincott, and Jon Wätte for their C++
wizardry and personal patience, and to Pete and Tom for reviewing this article.*