Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

C Programming


SEP93: C PROGRAMMING

A while back I plugged Philippe Kahn's latest musical venture, a compact disk with performances featuring Philippe in the company of some heavyweight jazz musicians. I said then that I did not know how you could get the disk. At the Borland International Conference in San Diego I asked Philippe about it. "You can't," he said. It seems his legal advisors were worried about the image of a CEO who was off playing music while the company foundered in the marketplace, so they put the kibosh on any distribution of the disk. Too bad, because it really is a nice piece of work.

The conference was the usual Borland bash. David Intersimone, resident BI evangelist and morale officer, roamed the hallways, parties, and meeting rooms with a camcorder, capturing quotes and happenings to share with the poor BI employees minding the store in Scotts Valley. He even climbed the podium and scanned the crowd as we waited for the keynote address. That keynote consisted of Philippe telling us how software was going to be developed in the next decade and Andy Grove, CEO of Intel, peddling the Pentium. Given my bad hearing, a cavernous hall with the acoustics of the Grand Canyon, and their accents, I couldn't keep up.

Errors and Intercepts

Exception handling is the next hot C++ language feature. Using it, a program can intercept and process exceptional conditions--errors, usually--in an orderly, organized, and consistent manner. Exception handling is not implemented in many C++ compilers yet. The only released MS-DOS compiler that supports the feature is the new Watcom C++ compiler. (I'll be looking at the Watcom compiler in a future column.) I first discussed exception handling in a tutorial book called Teach Yourself C++, Third Edition, (MIS:Press) which came out this summer. That discussion and this one are not based on any hands-on experience but rather on what I've read about the subject, so the code fragments that I use for examples might not be quite right. Furthermore, my reactions to how the feature is specified and how it might be implemented are not to be taken as gospel. Pragmatic programmers can view exception handling as an experimental feature until more compilers implement it with consistent behavior and interface. It will be, however, a powerful and useful language feature, and I look forward to the time when all the compilers include it and we can all use it.

Bjarne Stroustrup describes exception handling in The C++ Programming Language, Second Edition (Addison-Wesley, 1991). The Annotated C++ Reference Manual, by Ellis and Stroustrup (commonly called the ARM, Addison-Wesley, 1990) defines exception handling, too, so, the feature isn't new, having been described in print for about three years now. It's just that there are few compilers available that implement it.

Exception handling allows one part of a program to detect and report exceptional conditions and another part to handle them. This is an appropriate order. The classes and functions in libraries typically know how to detect errors without necessarily knowing what to do about them. An application's code will understand how to deal with errors but cannot always detect them. Exceptions are not restricted to errors, by the way. A programmer can use exception handling to manage all kinds of variable finite-state machine architectures. In practice, however, we think of exceptions as error conditions, or at least as unexpected conditions for which the program makes necessary side trips. The underlying theory is that a function, which can be buried many function calls deep and which may be hidden in a library, detects an exception that it cannot deal with and that it must report to the application at the surface, which must deal with the exception.

Consider a math-library function that detects overflow, underflow, divide-by-zero, and other error conditions in its input data. Is the error one that is caused by user-entered data or one that the program computed internally? Is it a validation condition or a bug in the program? The library function doesn't know, so an appropriate error-handling strategy depends on the application. The program might display an error message on the console or in a dialog box; it might ask the user to enter better data; it might terminate itself. Library functions shouldn't presume to know the best exception-handling strategies for all exceptions in all applications. On the other hand, applications cannot know how to detect all possible exceptions.

This division of responsibility for error detection, reporting, and handling isn't new. C libraries have supported it for years. Exception handling in C++ adds to the strategy an orderly way for the exception detector to report the exception to the exception handler. Somehow, the detecting function must return control to the handling function through an orderly sequence of function returns. The detecting function can be many function calls deep. An orderly return to the higher level of the handler function requires, at the very least, a coordinated resetting of the stack.

Exception Handling in C

C programs usually test library function return values for errors, and they use setjmp and longjmp to manage exceptions. The first approach, which uses things such as errno and NULL or ERROR function return values, is reliable but tedious. Programmers sometimes avoid or overlook some of the possibilities. The setjmp/longjmp approach is closer to what C++ exception handling does: it provides an orderly and automatic way to reset the stack to its state at a specified place higher in the function call hierarchy.

For example, a compiler's syntax checker can be many levels deep in a recursive descent parser when it detects a syntax error. The compiler doesn't need to terminate. Instead, it wants to report the error and go back to where it can read the next line of code and continue. The program uses setjmp to identify that place and longjmp to get to it. The longjmp call resets the stack to its state as recorded in the jmp_buf by the setjmp call. The initial setjmp call returns 0. The longjmp call jumps to a return from setjmp, which makes setjmp seem to return the specified error code, which should be non-zero.

There are anomalies in this scheme, however. As you'll soon learn, C++ exception handling doesn't solve them, either. Consider a function like Example 1. Forget for the moment that a real program would test for exceptions to fopen and malloc. The two calls represent resources that the program acquires before and releases after the longjmp. The calls could be in functions that (1) are called after the setjmp operation, and (2) have themselves called the parse function. The point is that the longjmp occurs before those resources are released. Therefore, every exception that this program detects represents two system resources that are lost--a heap segment and a file handle. In the case of the FILE* resource, subsequent attempts to open the same file would fail. If each pass through the system opened a different file--a temporary file with a system-generated file name, for example--the program would fail when the operating system ran out of file handles. We programmers traditionally solve this problem by structuring our programs to avoid it. Either we manage and clean up resources before calling longjmp, or we do not use longjmp when there are interim resources at risk. In Example 1, the problem is solved by moving the longjmp below the free and fclose calls. It isn't always that simple, however.

Resetting the stack in a C program involves resetting the stack pointer to where it pointed when setjmp was called. The jmp_buf stores everything that the program needs to know to do that. This procedure works because the stack contains automatic variables and function return addresses. Resetting the stack pointer discards the automatic variables and forgets about the function return addresses, all of which is correct behavior, because the automatic variables are no longer needed and the interim functions will not be resumed.

Exception Handling in C++

Using longjmp to unwind the stack in a C++ program does not work, however, because automatic variables on the stack include objects of classes, and those objects need to execute their destructor functions. Consider Example 2. No doubt the constructor for the String class allocates memory for the string value and its destructor deletes that memory. If an exception occurs, the String destructor does not execute because longjmp resets the stack and jumps to setjmp before the String object goes out of scope. The memory used by the String object is freed, but the heap memory pointed to by a pointer in the String object is not deleted because the destructor does not execute.

This is a problem that C++ exception-handling solves. The exception handling throw operation--the analogue to longjmp--unwinds the stack in a way that calls the destructors of all the automatic objects. If the throw occurs from within the constructor of an automatic object, the object's destructor is not called, although the destructors of objects embedded in the throwing object are called.

C++ functions that can sense and recover from errors execute from within a try block that looks like Example 3(a). Code that is executing outside of any try block is not able to detect or handle exceptions. Try blocks may be nested. The try block typically calls other functions that are able to detect exceptions.

A try block is followed by a catch exception handler with a parameter list as shown in Example 3(b). There can be multiple catch handlers with different parameter lists, one handler after the other in the source file. Each catch handler is distinguished by its parameter list. The parameter itself does not have to be named. A named parameter declares an object, and the exception detection code can pass a value in the parameter. If the parameter is unnamed, the exception detection code can jump to the catch exception handler merely by naming the type.

To detect an exception and jump to a catch handler, a C++ function issues the throw statement with a data type that matches the parameter list of the proper catch handler. The throw statement unwinds the stack, cleaning up all objects declared within the try block by calling their destructors. Next, throw calls the matching catch handler, passing the parameter object.

Example 3(c) begins to bring it all together. In this example, the program enters a try block, which means that functions called directly or indirectly from within the try block can throw exceptions. In other words, the foo function can throw exceptions and so can any function called by foo, and so on.

The catch exception handler function immediately following the try block is the only handler in this example. It catches exceptions that are thrown with an int parameter.

Catch handlers and their matching throw statements can have a parameter of any type. The parameter may be an automatic variable within the block that uses throw, even if the catch parameter list specifies a reference. In this case, the throw statement builds a temporary object to pass to the catch handler. The automatic object in the throwing function is allowed to go out of scope. The temporary object is not destroyed until the catch handler completes processing.

When a try block has more than one catch handler, a throw executes the one that matches the parameter list. That handler is the only one to execute unless it throws an exception to execute a different catch handler. When the executing catch handler exits, the program proceeds with the code following the last catch handler.

You can specify the exceptions that a function may throw when you declare the function as shown in Example 3(d). If a function includes such an exception specification, and the function throws an exception not given in the specification, the exception is passed to a system function named unexpected(). The unexpected function calls the latest function named as an argument in a call to the set_unexpected function, which returns its current setting. A function with no exception specification can throw any exception.

A catch handler with ellipses for a parameter list catches all exceptions. In a group of catch handlers associated with a try block, the catch-all handler must appear last.

You can code a throw with no operand in a catch handler or in a function called by one. The throw with no operand rethrows the original exception.

An uncaught exception is one for which there is no catch handler specified or one thrown by a destructor that is executing as the result of another throw. Such an exception causes the terminate function to be called. You can specify a function for terminate to call by calling the set_terminate function, which returns its current value. If no set_terminate function call has been made, terminate calls abort.

You must decide in your design how to differentiate the exceptions, and code the catch handlers with their distinguishing parameter lists. You might code only one catch handler with an int parameter and let the value of the parameter determine the error type. This approach assumes that you have control of all of the throws in all of the functions in all of the libraries that you use.

No doubt conventions will emerge. One way is to use class definitions to distinguish exceptions and categories of exceptions. A throw with a publicly derived class as its parameter is caught by a catch handler with the base class as its parameter. Consider Example 4. FileError is a public virtual-base class. Its derived classes are NotFound and Locked. The only catch handler for this category of exception is the one with the FileError reference parameter. It does not know which of the exceptions was thrown, but it calls the HandleException pure virtual function, which automatically calls the proper function in the derived class. Note that I am not recommending this particular set of exceptions for the fstream library; I am merely using the example to illustrate the technique. There might be good reasons to avoid throwing exceptions from within standard stream libraries. I'll address that concern later in this discussion.

There are other ways to enumerate exceptions. Instead of classes, you can use enumerated types and have switch statements in the catch handlers. Publishers of libraries will document the conventions that they use to throw exceptions, and your catch handlers will use those conventions, perhaps using several different conventions in one application. Eventually standards must emerge. If they do not, chaos will reign, and we will be reminded of the function and variable name collisions that many incompatible C function libraries create.

Recall the discussion earlier about the setjmp/longjmp anomaly with unreleased resources. C++ exception handling does not solve that problem. Consider the condition in Example 5 where the String object is allocated from the heap by the new operator. If the exception is thrown, the delete operation is not performed. In this case, there are two complications. The memory allocated on the heap for the String object is not released, and its destructor is not called, which means that the memory that its constructor allocated for the string value is lost as well.

The same problem exists with dangling, open file handles, unclosed screen windows, and other such system resources. If the program just shown seems easy to fix, remember that the throw could occur from within a library function far into a stack of nested function calls.

Programming idioms have been suggested that address this problem. In the second edition of his book, Dr. Stroustrup suggests that all such resources could be managed from within automatic instances of resource-management classes. Their destructors would release everything as the throw unwinds the stack. Another approach is to make all dynamic heap pointers and file and window handles global so that the catch handler could clean everything up. These methods sound cumbersome, however, and they work only if all of the functions in all of the libraries cooperate.

Exceptions in the Standard C++ Library

The ANSI X3J16 C++ Standards Committee is adopting and adapting the Stroustrup specification for exceptions. Exceptions are having significant impact on X3J11's library committee, because they are moving toward a standard that folds exception handling into those library functions that can detect exceptions. It is properly argued that a language which includes exception handling strategies as an integral language component is in bad company if its library does not make good use of the feature.

But consider the implications of a class or function library that throws exceptions. Programs using that library cannot ignore exceptions unless it is acceptable for them to abort when the exceptions occur. That's both good news and bad news. Programs should not, as a rule, ignore exceptions, and such a library can enforce that rule. Programs that allocate memory from the heap without testing, for example, should expect to be tossed out if the heap is exhausted. In the old days, those programs proceeded until a dereferencing of the null pointer took its nasty toll. With an exception-savvy new operator, however, those programs will abort if they do not bother to catch the exception. Programmers who object to having the new operator throw an exception can always use set_new_handler to override the behavior.

I think that was the good news. So what's the bad news?

The heap-exhaustion example is one that is typically used to justify libraries that throw exceptions. There are, however, legitimate programming idioms that ignore--or, at least, defer--the management of some exceptions. For example, you might not always want to test immediately after a you open a file that the file does not exist. The FileError example described earlier did just that but it might not be what you want to do. Furthermore, if an exception-throwing library is a new version of an older one that didn't throw exceptions, existing code that uses the library might break.

We worry about the X3J16 committee inventing language because their predecessor X3J11 C committee wasn't supposed to do that. Prior art is the best way to test the viability of a new and highly complex language feature. If it works in the trenches it will wash in the standard. The reverse might not be true. Understanding that, the X3J16 library committee is proceeding carefully in applying this concept. The principal issues are: Which library functions should throw exceptions; and which exceptions should they throw. One extreme would ignore exception handling altogether, furthering the C tradition where programmers are responsible for everything and programs that ignore exceptions crash. The other extreme would throw exceptions for all variable conditions that all library functions detect, in which case we might as well change the name of the main function to try. Somewhere in between is nirvana, and the committee is searching for it.

The Consequences of Exception Handling

Exception handling as a C++ language feature has consequences for C++ programmers because it changes the language. It is not a planned, well-integrated change, however. It is an add-on, an accoutrement, an afterthought. Because of its heritage, C++ already has constructs that tend to get in the way of what exception handling tries to do, giving rise to contrived solutions such as the resource-management classes suggested by Stroustrup, solutions that need constant nurture and attention to keep them healthy.

Besides the additional burden of understanding levied on the programmer, there is a performance penalty that an inadequate compiler implementation can exact. It makes one wonder about how this thing will work. To properly unwind the stack, the run-time code needs to maintain a trail from the throw back up through all the function returns and block exits to the try so that it can call automatic object destructors in an orderly sequence. Does every call to every function and every exit from every block need to test to see if it should resume normally or if a throw is telling it to ignore everything immediately following and proceed to its own exit? The outer application cannot even benefit from the compiler's presumed knowledge that code is executing outside a try block because every function except main is subject to a call from within one, and the binding occurs at run time not at compile time.

How about library functions? Will they all have the unwinding overhead? It seems to me that they would have to. What about extern "C" functions? How will the compilers be smart enough to know not to return to them? They will certainly not have exception-handling unwinding tests and they would continue processing merrily along as if the exception had not happened. Will C programmers pay the price because strcmp needs to participate in C++ exception handling? I hope not. You couldn't simply reset the stack of every C function the way longjmp does. A C function can call a C++ function through a callback pointer, and that C++ function needs an unwind even if the C function does not.

If you do not unwind the stack by a controlled sequence of returns up through the function tree, then each instantiation of an automatic object must be recorded somewhere in near-global space so that the throw can call the destructors. That in itself represents a significant overhead because it adds hidden processes to constructors. Can the compiler infer from the absence of a declared destructor that a class does not need destruction during the unwinding? These, of course, are the meanderings of an uninitiated outsider. I don't know how to implement the feature because I haven't tried to solve the problem. I'm having trouble figuring a reliable way to make it work badly much less efficiently. The implementers will no doubt be wrestling with all of these issues. Bjarne Stroustrup says that he implemented exception handling in the laboratory before he proposed its specification in the ARM. Watcom has a compiler that implements it now. How well these improvements work remains to be seen.

I hope they work well because the concept of exception handling is a Good Thing. We need to understand it so that we can exploit it in our designs. We need to expand our understanding of the libraries that we use to know which exceptions they throw and what our programs should do about them. We need to analyze the exception strategy of every library to ensure that careless library designers do not create potential collisions of the exception-identifying parameters. We need to be mindful that exceptions thrown from somewhere in a library can strand dynamic resources unless we meticulously plan for each such circumstance. Finally, we need to keep a watchful eye for new versions of tried and true libraries that now throw exceptions where they never did before.

Plauger's Purpose

From July 1986 until June 1993, P.J. (Bill) Plauger wrote a column for Computer Language magazine called "Programming on Purpose." His tenure ended when the magazine changed name and emphasis and, essentially, ceased to be. His audience would now be elsewhere, it seems. I'm not sure, but I think that the seven years he spent at that assignment is a record for a programmer-oriented column in a national magazine. Plauger's columns were a breath of fresh air. Always the pragmatist, he took aim at icons, disintegrated graven images, and constantly reminded us that far more code rises from the trenches than floats down from the towers. He stumps the lecture circuit, too, addressing conferences of programmers, teaching his sometimes irreverent, often entertaining, and always on-target philosophies about programming. He is an anomaly, a good programmer who can speak and write.

The "Programming on Purpose" columns are now available in three volumes from Prentice Hall. They are called, of course, Programming on Purpose, and the volumes are subtitled, "Essays on Software Design," "Essays on Software People," and "Essays on Software Technology." I have the first volume and will soon have the other two. Bill follows each essay with an "afterword," which allows him to reflect today on what he wrote in the past and how his views have held up in light of subsequent developments in the industry. Most stood the test of time rather well, I thought.

New things and ideas are usually received by the elders of the community in one of three ways: non-negotiable utter rejection on one end; zealous and rhapsodic total immersion on the other; and open-minded skepticism somewhere on the fence. Reading these essays, it was fun to watch Dr. Plauger approach object-oriented programming with caution and suspicion and then eventually accept it, but for different reasons than the popular ones. I remember a discussion we had about C++ several years ago. He was reluctant to give it full measure, saying, "I'm a procedural kind of guy." Now he is an active participant in the formal standardization of the C++ language.

This trilogy takes its place next to the writings of Brooks and Weinberg in my collection of books necessary for healthy and happy attitudes toward programming. Highly recommended.

Example 1:

void foo()
{
    FILE *fp = fopen(fname, "rt");
    char *cp = malloc(1000);
    /* parse the input */
    /* ... */
    if (error)
        longjmp(jb, ErrorCode);
    free(cp);
    fclose(fp);
}

Example 2:

void parse()
{
    String str("Parsing now");
    // parse the input
    // ...
    if (error)
        longjmp(jb, ErrorCode);
}

Example 3:

(a)
try  {
    // C++ statements
}
try  {
    // C++ statements
}
catch(int err)   {
    // error-handling code
}
(c)
void main()
{
    // --- the try block
    try  {
        foo(); // call a lower function
    }
    // --- catch exception handler
    catch(int errcode)   {
        // error-handling code
    }
}
// --- program function
void foo()
{
    // C++ statements to do stuff
    if (error)
        throw -1;
}
(d)
void f() throw(char *, int)

Example 4:

class FileError {
public:
    virtual void HandleException() = 0;
};
class Locked : public FileError {
public:
    void HandleException();
};
class NotFound : public FileError {
public:
    void HandleException();
};
void bar()
{
    try  {
        foo();
    }
    catch(FileError& fe)   {
        fe.HandleException();
    }
}
void foo()
{
    // ...
    if (file_locked)
        throw Locked();
}

Example 5:

void foo()
{

    String *str = new String("Hello, Dolly");
    // ...
    if (file_locked)
        throw Locked();
    delete str;
}


Copyright © 1993, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.