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.

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:

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.

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().