"The (B)Leading Edge"
Exceptions and Debugging

by

Jack Reeves
©1996 - The C++ Report
July 14, 1996

With this installment of "The (B)Leading Edge", I want to step back from specific details of how to use C++ exceptions, and look more at the issue of what to use them for. Overall, the literature on C++ exceptions seems to lean heavily in favor of NOT using exceptions for debugging. As always, Bjarne Stroustrup's writings on the subject are both illuminating and sensible. In Chapter 9 of [2] he makes the case: "The C++ exception handling mechanism is designed to support error handling. In particular, it is intended to support error handling in programs composed of independently developed components". He goes on to say: "... the primary aim of the exception mechanism ... is error handling and the support of fault tolerance." Another author made the remark (paraphrased) "If an exception is not going to be handled, what is the point of throwing it?" Originally, I agreed with this philosophy.

Like many C programmers, I used the assert macro as an aid to debugging. When I first started looking at how to use exceptions effectively, I had no reason to think that I would change my approach. In fact, I figured that one of the first guidelines I would formulate would be something along the lines of "Don't try to use exceptions for debugging, use assert() statements instead." You have not seen such a guideline, and you will not. I rapidly concluded that programmers would much rather throw exceptions than use the assert macro. I myself no longer use assert statements in my code at all.

Just in case you haven't ever used it, the assert macro is part of the Standard C Library, and is carried over unchanged into the (draft) Standard C++ Library. In its virulent form, an assert(x) macro is replaced with something similar to this:

((x) ? (void)0 : _assert(#x, __FILE__, __LINE__))

If the condition is true, nothing happens, otherwise, the _assert() function is called. This is a helper function which typically writes an error message to stderr, and then calls abort(). One of the key points of the assert() macro is that it can be disabled. If the macro NDEBUG is defined at compile time (actually whenever the header "assert.h" is included), the macro is redefined to be something like:

((void)0)

i.e. a null statement.

The general rational for the use of assert macros is that they allow you to make explicit the assumptions your code depends upon. During debugging, the runtime test verifies the assumption, aborting the program with an error message if the assumption does not hold. After you have verified that your code works correctly, you can rebuild it with the asserts disabled to avoid the runtime overhead of the tests, but the macros remain in the code as documentation. All of this seems eminently logical. Therefore, you would think that more programmers would use assert macros than seem to. You would also think that programmers would not overwhelmingly prefer to throw exceptions instead of using assert macros.

Nevertheless, this is what I have found: programmers overwhelmingly prefer exceptions to using asserts. In order to understand why this is so, we have to drag into the light one of the open secrets of software development. It is the nature of software that errors fall into two broad categories: runtime errors and logic errors. The (draft) C++ Standard Library includes two standard exception base classes with these names, but I want to make it clear that here I am talking about the errors themselves, not the exceptions. Any given error may, or may not, result in an exception of the same class. Many errors may not result in any exception at all. Some may be handled in more traditional ways, and some may not be detected at all.

There are certainly programming domains where runtime errors are very real possibilities. Communications protocols immediately come to mind, as do hardware device drivers, operating systems, and anything that has to support complex interaction with human operators. However, well before the ascendance of C++, software developers learned that the only way to deal with error prone problem domains was to wrap them in simplifying abstractions. First we had procedural abstractions, now we have object oriented abstractions. When one level of abstraction is not enough, we use layers. This approach is so well known that in the area of data communications it has attained the level of an international standard in the OSI model. While the possibility of runtime errors still exist (and should not be ignored), the reality for most of us is that the vast majority of errors we will ever encounter in our programming are logic errors.

The vast majority of possibly errors in typical software are logic errors.

Logic errors are mistakes in the software itself. Therefore, by definition, correcting a logic error means changing the software. This, in turn, means that you can not write code to "handle" a logic error. On the other hand, since software does not wear out or change from usage, if you can just get it right, you don't have to worry about logic errors. This leads to a rather dangerous attitude towards logic errors. We assume that if we can just find all of our logic errors during debugging, then we can eliminate the code which tests for such mistakes and avoid the performance overhead of a bunch of unnecessary runtime checks in our distributed applications. The assert macro is explicitly designed to cater to this assumption.

Unfortunately, things are never that simple. Most of us have learned to be very reluctant to remove certain types of debugging code from our software. I want to emphasize the phrase certain types. Most of us have enough ego that we do not hesitate to remove debugging code intended to trouble-shoot our own software after we feel we have gotten that software right. Maybe we leave a comment about an assumption, but usually not even that. What we do not like to do is remove the error checks that protect our software from errors in other software.

What has all this got to do with exceptions? A lot! When I looked back over some of my code that still had asserts, I found three general categories for their use:

i. Rudimentary error checking. I was very fond of stuff like the following:

char* buf = new char[256]; assert(buf);

and

istream is("config.txt"); assert(is);

ii. Parameter checks. For example:

X get(int i) {

assert(i >= 0 && i < size);

return val[i];

}

iii. A stopper for code paths that should not be entered, as in:

int find(X& x) {

for (int i = 0; i < size; ++i) {

if (val[i] == x)

return i;

}

assert(false);

}

and

switch (something) {

case 1 : // ...

case 2 : // ...

case 3 : // ...

default :

// something should always be 1, 2, or 3

assert(false);

}

All of these cases represent situations where the cause of the error was out of my control, even the third category. In all three categories, errors are being detected in one place, but they have to be handled in some other place. In category(i), the errors may be either logic errors or runtime errors, but in either case my code could not handle the error, and was not designed to propagate the error up the call chain. In categories (ii) and (iii) it is my code that detects the error, but again, there is nothing I can do about the problem, and the software was not designed to report such errors. This sounds like precisely the types of situations for which exceptions were intended.

In fact, in (draft) Standard C++ all of the above examples represent cases where built-in exceptions are already provided. In category(i), the new operator will throw a bad_alloc exception instead of returning a null pointer. The iostreams library will not throw exceptions by default, but they can be enabled, in which case the second example will result in an ios_base::failure exception. The category(ii) example should be an out_of_range exception, while those in category (iii) both can be represented with an invalid_argument exception.

In situations like this, I (and most other developers that I know) invariably prefer to throw an exception. The exception stops the code from executing incorrectly (which may have aborted the program just as surely as the assert macro would), but it has the distinct advantage that it does not immediately terminate the program. Instead it refers the problem back up the call chain. What happens then is not my immediate problem, but becomes the concern of my client. As confirmation of this point of view, consider that the (draft) Standard C++ Language specification defines 4 standard exceptions that can result from the constructs of the language itself (as opposed to occurring in the library): bad_alloc, bad_cast, bad_typeid, and bad_exception. Of these, it can be argued that only bad_alloc represents a runtime error (and is actually a library error), while the other three all represent coding errors of one sort or another. Likewise, most of the exception conditions defined in the (draft) Standard C++ Library specification represent logic errors. So, assuming that exceptions are going to replace assert macros as the debugging tool of choice in Standard C++ programs, let's do some comparisons of how the two work and see if we can make our exception based debugging at least as useful as our old assertion based debugging.

First, we note that one good thing about the assert macro was that it was very easy to use. Obviously, we can obtain a similar ease of use for our exception based version with a macro designed for that purpose:

#define ASSERT(x) \

((x) ? (void)0 : _logic_error(#x,__FILE__,__LINE__))

A possible version of the helper function _logic_error() is shown in listing 1. It uses an ostringstream object to construct the string to be passed to the logic_error constructor (class ostringstream is similar to the older ostrstream, which it replaces). Macros for more specific exceptions such as out_of_range and length_error are similar. Besides ease of use, these macros also give us one way of fulfilling Throwing Guideline #7 from [5] (Give the client a way to disable unwanted runtime tests). For this, we provide our macros with the same type of compile time switch as the standard assert macro.

Next, let's assume right off the bat that we are not running a debugger when the exception occurs. These days, most good C++ debuggers are reasonably competent in coping with exceptions, and will provide a range of options for setting breakpoints on the occurrence of an exception. In such a case, a thrown exception is at least as good as a failed assertion, and is often better.

An uncaught exception has the same effect as an assertion failure...almost.

If we are not running a debugger, an uncaught exception has the same effect as an assertion failure...almost. An uncaught exception will propagate out of main(), which will call terminate(), which will call terminate_handler(), which in the default case will call abort(). There are possibly three things which are different from the assertion failure case. The first you have no control over. On most systems, the call to abort() results in a core dump. With the right tools, this can provide quite a lot of information about the state of the program when it aborted. The assert macro basically calls abort() directly. With an uncaught exception, the (draft) Standard leaves it up to the implementation whether the stack is unwound before terminate() is called. If the stack is unwound, then the core dump is not going to contain much useful information about the state of the program when the exception occurred. Hopefully, most implementations will recognize the debugging value inherent in not unwinding the stack when an uncaught exception invokes terminate(), so in practice this should not be a real difference.

The second possible difference is that the assert macro will generate a diagnostic message. This message will contain the information provided by __FILE__, and __LINE__, as well as a description of the reason for the failure. No diagnostic is required when an uncaught exception terminates the program. Some implementations do provide a diagnostic when an exception goes uncaught. Usually this diagnostic will include the type of the exception that was propagating. While better than nothing, this diagnostic does not contain any information about what caused the exception, or where it came from. We could change the helper functions for our exception macros so that they generate a diagnostic message exactly like the _assert() helper function, but there are other problems with this approach which we discuss below.

The third possible difference between an assertion failure and an uncaught exception is that while aborting the program is the standard behavior for an assertion failure, it is just the default behavior for an uncaught exception. The default terminate_handler() can be replaced by a program defined version. A user defined version of terminate_handler() has to terminate the program, but it does not have to call abort().

From this it is fairly easy to conclude that we probably want to do better than just let our exceptions propagate uncaught out of main(). One key point has to be repeated, however: while uncaught exceptions may be less useful from a debugging standpoint, they are equivalent to the assert macro in adding robustness to our code. We can freely switch from assert macros to exceptions in our code, even in legacy applications, without being required to make changes throughout.

With that said, we now get to some of the problems with assert macros that using exceptions can overcome. When a program terminates with either a failed assertion or an uncaught exception, the programmer has little control over the process. In GUI environments (or embedded systems), stderr (or cerr) may not exist. In such cases, it is not unusual for a program to just silently go away. Likewise, in many cases, a program abort will leave dangling resources such as open communications lines, or locked database records. Even in a debugging environment, this can become a problem. If we are using exceptions, we can deal with these problem.

Guideline #1 -- Always include a try/catch block around main. Catch all exceptions and generate an appropriate diagnostic.

I think the best way to do this is with a function-try-block:

int main()

try {

//...

} catch (exception& ex) {

cerr << "caught exception in main, what = "

<< ex.what() << endl;

} catch (...) {

cerr << "caught unknown exception in main" << endl;

}

This example uses cerr, but a GUI application could put up a message box, and an imbedded application could write to an error log or send the appropriate message to earth, or whatever. I like to use a function-try-block in this case because there is no question that we are going to end the program (see the sidebar: Function-try-blocks). If there are any specific exceptions that we think we can handle, then a try-block within main() (or at whatever lower level is appropriate) can deal with them.

In order for this to work, we have to make sure that the exception reaches main(). Goal V of [4] (and its associated guidelines) is applicable here (Do not catch any exception you do not have to). Likewise Guideline #10 of [4] is vital (Always rethrow the exception caught in a catch (...) block). If we want to get any information out of the exception, Guideline 2 from [5] is important (Throw exceptions derived from the standard exception classes), though you can obviously add other catch clauses to the try-block if you know what other exception classes are possible.

Before we go any further, we need to discuss exception specifications. I briefly toyed with the idea that exception specifications could be used to assist debugging. After all, in a correct program, an exception caused by a logic error should not occur. Therefore, if one did occur, it would be an unexpected exception. Exception specifications are designed to stop unexpected exceptions, right? I quickly gave up on this idea. Trying to use exception specifications to block logic errors yields the worst of both worlds -- you don't get the diagnostic message of an assert, the exception does not propagate to main() where its information can be displayed, and the program core dump (via terminate()) takes place after the stack unwinds to the point of the current function, making it less useful. Furthermore, the typical attempt to write such an exception specification usually starts out as throw(). Besides logic errors, this will block the propagation of bad_alloc, and any other runtime error. As I discussed in my previous column[6], trying to write exception specifications that correctly take into account the possible exceptions from lower level routines is very difficult. Exception specifications are not a debugging tool, rather they are an interface documentation tool.

Guideline #2 - Make sure your exception specifications can pass logic error exceptions.

Some people may disagree with that and prefer the opposite (who knows, in 6 months I may disagree with it, but I doubt it). If you have any real experience, please let me know. My experience so far has been that exception specifications are more of a hindrance than a help. As a general rule, I avoid them.

Note that so far, we are not talking about radical changes in the way we write programs. This is deliberate. Debugging is still debugging, and the hard parts of programming are still going to be hard. There is no point in making them more complicated, so we make things as simple and as similar to current techniques as possible. When using exceptions, however, we now have some new possibilities.

One change I made to my debugging toolkit is not really new, but I will start with it. I have always thought the diagnostic message from the assert macro left much to be desired. Without access to the source code where the error occured, it is essentially useless. About the best that can be said for it is that it takes almost no effort on the part of the developer to generate it. Since our exception macros are going to propagate the error message as part of the exception, it seemed appropriate to put more useful information in the diagnostic. The macro became:

#define ASSERT(x, f) \

((x) ? (void)0 : _logic_error(#x, f))

The second argument to the macro is a string which identifies the function. While this does take more work on the part of the programmer, it provides far more useful information.

With this change, an immediate question is why continue to use a macro. Originally we had no choice (because of __FILE__, and __LINE__), but no longer. Nevertheless, I kept the macro instead of switching to an inline function. One advantage that I discovered was that within a class library, I could provide aprivate version of _logic_error(). Originally, my primary motivation for doing this was to simplify using the macro; within the class specific helper function, I could add the class name to the diagnostic string without having to supply it on every invocation of the macro. For example:

void

MyClass::_logic_error(const char* err, const char* func)

{

string what = "Assertion failure:";

what.append(err);

what.append(" in MyClass::").append(func);

throw logic_error(what);

}

I quickly discovered other uses for class specific helper functions for my exception macros.

One simple but interesting case occurs in templates. With a little application of RTTI we can provide the instantiated name of the class.

void MyClass<class T>::

_logic_error(const char* err, const char* func)

{

const type_info& infoT = typeid T;

string what = "Assertion failure:";

what.append(err);

what.append(" in MyClass<");

what.append(infoT.name());

what.append(">::").append(func);

throw logic_error(what);

}

The typeid operator is new to C++. Typeid was originally added to C++ to allow programs to obtain information about the actual class of an object being referred to via a polymorphic reference. As defined in the (draft) Standard the typeid operator is more general purpose. In the first place, it can be applied to any expression. If the expression does not yield a polymorphic reference, the returned type_info object describes the static type of the expression. Typeid can also be applied to a type-id, in which case it yields a type_info object that describes the type. This is what is being done here. I use it to get a string with the type name of the type used to instantiate the template.

We can take this idea one step further.

void MyClass<class T>::

_logic_error(const char* err, const char* func)

{

const type_info& infoT = typeid *this;

string what = "Assertion failure:";

what.append(err);

what.append(" in ").append(infoT.name()).append(func);

throw logic_error(what);

}

Here I use typeid to get a string which contains the name of the actual type of object used to invoke the function where the error was detected. This is pretty minor, but I find it both fascinating, and very satisfying to see how various features of the language have been generalized and can be used together in new and useful ways.

Once I had private helper functions for my various exception macros, it was a fairly short step to also add a central error function as I described in Guideline 8 of [5] (Consider giving the client a way to replace the error reporting mechanism with a client defined version). In [5] I described the advantages of running all exceptions from a class through a single error reporting function. That function could then provide a means for clients to override the default error reporting mechanism if they so desired. Combined with our macro helper functions we get the code in listing 2. There a few real quirks in this code, but they are there for a reason.

The first thing you notice is that the _logic_error() helper function throws the exception, catches it immediately, and then calls the common _error() function. The reason for this is so that _error() can rethrow the original exception. If we try to throw the exception passed to _error(), we do not get the behavior we desire (see Guideline 8 of [5] for more explanation). Also note that _error() will go ahead and rethrow the exception if the error_handler() function returns. Again, this is a deliberate choice. This limits what a client supplied error_handler() can do. It must either rethrow the original exception (or return), throw a different exception, or terminate the program some other way. If a client supplied error_handler() actually handles the exception -- which it can do as in:

try {

throw;

} catch (...) {}

return;

then the throw; in _error() will call terminate(). This is what we want.

While [5] discussed some of the more esoteric reasons why you might want to let a client override error_handler(), there are far more mundane uses for the capability: e.g. during debugging you can supply an error_handler() function that logs the message from the exception before that exception goes propagating up the stack (and maybe crashing the program you are trying to debug).

At this point we have exceptions being thrown to indicate errors, we have put some useful diagnostic messages into the exceptions, and we have a catch clause in main() to display the diagnostics when the exception occurs. The one thing we lack is information on the path from the exception site to main(). We can get that, too.

Listing 3 shows how this works. First we create a simple Traceback class. We also define a macro to create a Traceback object in a function. We use this macro in any functions that we want to get traceback information from:

void foo()

{ TRACE("foo");

//...

}

(We use a macro so that a compile time switch can disable the creation of Traceback objects -- after all, you can not ignore efficiency). The Traceback destructor is the important part of this class. When the Traceback object is destroyed, it calls the standard library function:

uncaught_exception();

This function was added to the (draft) Standard C++ Library at the November 1995 meeting. It returns true from the time an exception has been created by a throw until the exception is caught. An exception is considered caught when a handler has been entered (or unexpected() has been called). The stack has been unwound when this happens. We use this function to determine that our Traceback object is being destroyed as a result of an exception unwinding the stack. (This is probably not the primary reason uncaught_exception() was added to the (draft) Standard, but serendipity strikes again).

When the Traceback destructor sees itself invoked by a stack unwind, it pushes its function name (saved by the constructor), onto the back of a vector which is a static object of the Traceback class. When the exception is finally caught in main(), the call_stack vector will contain pointers to the names of each function which had a Traceback object destroyed by the stack unwind. It becomes a simple matter to display this information along with the diagnostic contained in the exception itself.

In the listing, the ez_for_each() function is a helper function of the for_each<>() template defined in the algorithms section of the (draft) C++ Standard (Chapter 25). Such helper functions are described in Graham Glass's "STL in Action: Helper Algorithms" article[7]. (It is too bad such helper algorithms are not defined as part of the (draft) Standard itself).

This is just an outline of some of the things that can be done to enhance debugging in C++ by using some of the new features of the language, along with a container and a standard algorithm or two.

-- With our own ASSERT macro (or OUT_OF_RANGE, or LENGTH_ERROR, or whatever) we can add debugging exceptions to our code almost as easily as the old C Library assert macro.

-- These macros (actually their helper functions) include some useful information in the exception that is thrown.

-- By including these macros in inline functions and allowing them to be removed at compile time we can provide for Throwing Guideline #7 (Give the client a way to disable unwanted runtime tests).

-- We can override the helper functions of these macros to provide class specific information in the exceptions.

-- We can use the new RTTI operator typeid to get the actual type of the object used to invoke a class member function.

-- We can also use class specific versions of the exception macro helper functions to hook to a central class error function. Such a central function allows us to provide for Throwing Guideline #10 (Consider giving the client a way to replace the error reporting mechanism with a client defined version).

-- We can catch the exceptions thrown by these macros in a function-try-block of main() where we can generate an appropriate display of the contained information before we exit the program. Of course to do this, we have to make sure the exception gets to main() (see Coping Goal V (Do not catch any exception you do not have to), in particular Guideline #8 (Always use a catch (...) block to cope with propagating exceptions), and Coping Guideline #10 (Always rethrow the exception caught in a catch (...) block).

-- By catching the exception in main() we allow the stack unwind to take place. We can then call exit() (or just return) to allow the destructors for static objects to be invoked, as well as allowing any atExit() functions to run. This gives our program a clean exit.

-- We can use a simple class, along with a container from the STL portion of the (draft) C++ Standard Library to provide a listing of the functions which the exception propagates through on its way to main(). This takes some work and discipline on the part of the developer (and will obviously not work for any functions which we do not contain the necessary Traceback object). Nevertheless, I once developed a small program in a little over half a day that reads a C++ source file, finds all the function definitions, and inserts the necessary macro at the beginning of each. This allowed me to "instrument" a fairly large project (80,000+ lines of code) rather quickly. As a general rule, I find the disciplines needed in C++ coding far less arduous than those that many shops impose on their C programmers.

After spending this column looking at how to deal with (and use) exceptions that represent logic errors, it seems appropriate to step back and review the Goals and Guidelines presented in my own "Coping With Exceptions" article[4] as well as other writing on the subject [3] to see if we need to reconsider any of those ideas. The answer seems to be: "not really, but it does provide some perspective." The primary goals of coping with exceptions remain important -- you should always try to leave a resource in a good state when an exception occurs, or at least in a state where it can still be destroyed correctly. Nevertheless, the final Goal of [4] -- Don't get too paranoid -- takes on a new importance when you realize that the vast majority of exception sites in a well designed and well tested program are never going to ever result in an exception. That is because they represent tests for logic errors rather than actual runtime errors.

We should not get complacent in our use of exceptions, especially if we are trying to develop reusable software. Just because something seems like a logic error to you or me does not preclude the possibility that someone else will treat it as a runtime error. Runtime errors can be handled. Reusable software must treat every exception as if it was going to be handled. At the application level, however, we can relax a little. Our biggest concern will not be handling all the exceptions which will not be occurring, but rather making sure we do not mishandle the rare ones that do occur.

My final observation is the usual one: I have presented code examples which use several of the new features in the (draft) C++ Standard Language and the Library. Most compilers do not yet support these features, and Standard Libraries are not yet available. With the prime exception of function-try-blocks, most of what I have presented here can be faked rather easily using existing compilers that support exceptions and templates. For example, a kludged version of uncaught_exception() can test a global counter maintained by the exception base class to see if any exception objects exist. This will not provide the same functionality of the (draft) Standard version of uncaught_exception(), but it will do for the purpose described here.

Next time, I intend to look at something besides exceptions.

 

Listing 1

void

_logic_error(const char* c, const char* f, unsigned int l)

{

ostringstream os;

os << "Assertion failure:" << c

<< " in file=" << f

<< "; line=" << l;

throw logic_error(os.str());

}

 

Listing 2

void MyClass<class T>::

_logic_error(const char* err, const char* func)

{

const type_info& infoT = typeid *this;

string what = "Assertion failure:";

what.append(err);

what.append(" in ").append(infoT.name()).append(func);

try {

throw logic_error(what);

} catch (exception& ex) {

_error(ex);

}

 

void

_error(exception& ex) {

if (error_handler) {

(*error_handler)(ex);

}

throw;

}

Listing 3

class Traceback {

const char* func;

public:

Traceback(const char* f) : func(f) {}

~Traceback();

static vector<const char*> call_stack;

};

Traceback::~Traceback()

{

if (std::uncaught_exception())

call_stack.push_back(func);

}

#define TRACE(f) \

Traceback _debug_tb(f)

void print_stack_info(const char* f)

{ cerr << '\t' << f << '\n'; }

int main()

try {

TRACE("main");

// ...

} catch (exception& ex) {

cerr << "Caught exception in main\n";

cerr << ex.what() << '\n';

cerr << "Stack trace:\n";

ez_for_each(Traceback::call_stack, print_stack_info);

} catch (...) {

cerr << "Caught unknown exception in main\n";

cerr << "Stack trace:\n";

ez_for_each(Traceback::call_stack, print_stack_info);

}

 

References:

1. "Working Paper for Draft Proposed International Standard for Information Systems -- Programming Language C++", April 1995.

2. Bjarne Stroustrup, The C++ Programming Language - Second Edition, Addison-Wesley, 1991.

3. Harald Mueller, "10 Rules for Handling Exception Handling Successfully", C++ Report, Vol. 8, No. 1, January 1996.

4. Jack Reeves, "Coping with Exceptions", C++ Report, Vol. 8, No. 3, March 1996.

5. Jack Reeves, "The (B)Leading Edge: Exceptions and Standard C++", C++ Report, Vol. 8, No. 5, May 1996.

6. Jack Reeves, "The (B)Leading Edge: Guidelines for Exception Specifications", C++ Report, Vol. 8, No. 8, August 1996.

6. Bjarne Stroustrup, The Design and Evolution of C++, Addison-Wesley, 1994.

7. Graham Glass, "STL in Action: Helper Algorithms", C++ Report Vol. 8, No. 1, January 1996.

 

SIDEBAR

Function-try-blocks

Function-try-blocks are (apparently) a fairly late addition to the language, though they appear in the public release of the April 1995 Draft Working Paper[1]. They are not discussed in D&E [6], nor have I seen them discussed elsewhere. I have used some examples in my previous columns, and while the concept is fairly intuitive, a more detailed discussion seems in order.

The relevant parts of the DWP[1] read:

function-try-block:

try ctor-initializer-opt function-body handler-seq

"A function-try-block associates a handler-seq with the ctor-initializer, if present, and the function-body. An exception thrown during the execution of the initializer expressions in the ctor-initializer or during the execution of the function-body transfers control to a handler in a function-try-block in the same way as an exception thrown during the execution of a try-block transfers control to other handlers."

"Referring to any non-static member or base class of the object in the handler of a function-try-block of a constructor or destructor of the object results in undefined behavior."

"The fully constructed base classes and members of an object shall be destroyed before entering the handler of a function-try-block of a constructor or destructor for that object."

"The scope and lifetime of the parameters of a function or constructor extend into the handlers of a function-try-block."

"If the handlers of a function-try-block contain a jump into the body of a constructor or destructor, the program is ill-formed."

"If a return statement appears in a handler of a function-try-block of a constructor, the program is ill-formed."

"The exception being handled shall be rethrown if control reaches the end of a handler of the function-try-block of a constructor or destructor. Otherwise, the function shall return when control reaches the end of a handler for the function-try-block."

In a nutshell, this says that the body of a function, a constructor, or a destructor, can be a try block with its associated handlers. For the most part, wrapping a function body in a function-try-block is equivalent to having the body of the function be a single try-block statement. There are potentially three special cases where this is not true.

The most obvious special case for a function-try-block is on a constructor. Function-try-blocks close a hole in the language by allowing a constructor to catch exceptions which might be thrown by a base class constructor or by a constructor of a member object. Recall that constructors are special to the compiler. The body which you write is augmented by the compiler to invoke the constructors of the object's base classes, and member objects. This all takes place before execution of the constructor body. Before function-try-blocks, there was no way to catch an exception from one of these other constructors.

Note carefully that while a function-try-block will allow you to catch an exception from a base class or member object constructor, you can not handle the exception. You can not attempt to recover from an exception in, say, a member object and finish building the object -- an exception anywhere in the constructor chain will cause the partial object to be destroyed as part of the stack unwind process. This takes place before the handler is entered. Normal exit from a handler of a function-try-block rethrows the exception that was caught. You can not even attempt to avoid a memory leak by releasing memory you inadvisably allocated in the ctor-initializer list (see Guideline #6 in [4])-- attempting to access any members of the object is undefined behavior.

At this point, you may be wondering what good is a function-try-block. There is one thing, however, that you can do in a function-try-block on a constructor that can not be done any other way: you can rethrow a different exception from the one that was caught. This may seem trivial when you read about it, but can be of vital importance when trying to develop a clean abstraction. Stated another way, function-try-blocks allow us to put meaningful exception specifications on constructors (meaningful to the abstraction being represented), and make them work. For example:

class X {

Y y;

public:

class Error {}; // nested exception class

X::X(const Y& ay) throw(X::Error);

// ... details omitted

};

X::X(const Y& ay) throw(X::Error)

try

: y(ay)

{

// ...

} catch (...) {

// includes possible exceptions from Y::Y

throw X::Error();

}

The second special case for a function-try-block is on destructors. Like constructors, a destructor as written is augmented by the compiler with the necessary code to destroy all the member objects and the base classes. As with constructors, there is not a lot you can do in a function-try-block for a destructor. If you have an exception in a destructor, whether from the body itself, or from one of the member or base class destructors, the stack unwind is going to take place and attempt to finish destroying the object. The one special thing you can do in a destructor function-try-block is return. This handles the exception, and is potentially useful for preventing the propagation of an exception from a destructor.

You can also use a function-try-block on a destructor to convert an exception thrown from a member destructor into a more appropriate type.

The third potentially special case for a function-try-block is on function main(). I say "potentially" because this is my own interpretation of the DWP. Function main() is special -- it has implementation-defined type, implementation-defined linkage, you can not take its address, you can not call it recursively, etc. I find nothing in the DWP that says you can not put a function-try-block on main(). On the other hand, there is no discussion of the semantics of a function-try-block on main(), and there are some intriguing questions.

In some sense, main() contains aspects of both a constructor and a destructor. This has to do with initialization and destruction of non-local objects. Unlike base classes and member objects, the DWP does not specify when non-local objects are initialized, saying only that they must be initialized before use. The DWP does say that non-local objects are destroyed as part of the call to exit(). exit() can be called directly, or is invoked indirectly after a return from main() has destroyed all local objects of main().

Question 1: Can a function-try-block on main() catch an exception thrown from a constructor during dynamic initialization of a non-local object? Note: if the implementation does not do initialization of non-local objects before the first statement of main(), this question also applies to a try-block inside of main().

Question 2: If we tentatively assume that the answer to (1) is "yes", what can we do in the handler? More specifically: what assumptions (if any) can we make about the environment that exists when the handler is entered?

Question 3: What is the environment if we enter a function-try-block handler for main() as a result of an exception in the body of main(), i.e. will the stack unwind of main() call exit() or not?

Question 4: Can a function-try-block on main() catch an exception thrown by a destructor of a static object? If we assume "yes", see question 2.

Question 5: What happens after a return from a handler of a function-try-block on main()?

I have something slightly more than academic interest here. If a constructor for a non-local object throws an exception, and there is no way to catch it, the program will terminate. If this happens, other non-local objects that have already been constructed are left dangling since nothing invokes their destructors. In certain applications, this could be a real problem. I have said before [2], and say again: the design of truly fault tolerant software is difficult and involves far more than just throwing and catching exceptions. Nevertheless, if we want to use exceptions to build fault-tolerant programs, we have to be able to catch the exceptions that occur.

I would find it useful to be able to catch an exception thrown from the constructor for a non-local object. A function-try-block on main() is potentially the way to do this. Unfortunately, this runs head on into the problem that the order of initialization of non-local objects in different translation units is undefined in C++. This is not a new problem in C++, just a new twist on an old problem. There are various techniques we can use to guarantee that non-local objects that we want to use are, in fact, initialized before we use them, but none of these "techniques" are going to cope with an exception. Ultimately, any exception handler that can catch exceptions from the initialization of non-local objects is going to have to live with the fact that the only objects it can reliably use are what it declares on its own stack frame, and any non-local objects of POD (Plain-Ole-Data) type.

Personally, I think I could live with that restriction in order to have a way to catch those few potential exceptions that otherwise would simply cause a program abort. A true fly-in-the-ointment comes from question 4 above. If we assume that the function-try-block of main() encapsulates the dynamic initialization of non-local objects, it is reasonable to assume that it should also encapsulate their destruction. This implies that exit() be invoked within the scope of the function-try-block. This in turn opens questions of what happens if we invoke exit() from the body of a handler.

My preference would be to have a function-try-block on main() be able to catch exceptions from constructors for non-local objects, and not worry about exceptions from destructors of static objects. This way, if I had an exception during construction of a non-local object, I could invoke exit() within the handler and ensure that I destroyed any objects that had already been constructed (this implies that exit() knows which objects have, and which have not, been constructed, but this is not a new problem either).

I fear that without specific details in the C++ Standard, a function-try-block on main() may have different results in different implementations. This is not a good thing, but as with a lot of other things about Standard C++, we will just have to wait and see. While waiting, I am assuming that a function-try-block on main() works exactly like a try-block within main().

----------------------------------------------------------------