Robert Schmidt
Microsoft Corporation
December 16, 1999
Part 12 of this series on exception limitations inherent to implementations of unexpected.
Last time, I introduced the C++ Standard Library function unexpected and showed limitations in its Visual C++® implementation. This time, I want to show limitations inherent to all implementations of unexpected, along with ways to get around them.
I mentioned this one briefly in my previous column, but it bears amplification: The unexpected handler that traps and filters unexpected exceptions is global to and unique within a program.
All unexpected exceptions are handled by the same single unexpected handler function. The Standard Library sets the default to be called for every unexpected exception. You can trump that default handler with your own -- but as with the default, the Library calls your handler universally for all unexpected exceptions.
Unlike a normal exception handler, such as
catch (int)
{
}
an unexpected handler does not "catch" an exception. Once entered, all the handler knows is that some unexpected exception was thrown. The handler is ignorant of the exception's type and origin, and can't even appeal to the Library for help: No Library routine or object preserves the offending exception.
At best, an unexpected handler can bounce control to another party that might have more answers. Consider
#include <exception>
using namespace std;
void my_unexpected_handler()
{
throw 1;
}
void f() throw(int)
{
throw 1L; // oops -- *bad* function
}
int main()
{
set_unexpected(my_unexpected_handler);
try
{
f();
}
catch (...)
{
}
return 0;
}
f throws an exception it promised not to. As a result, my_unexpected_handler is called. That handler hasn't a clue of specifically why it was entered. Its only useful recourse -- beyond ending the program -- is to lob another exception, hoping both that the new exception is compatible with the violated specification, and that some other part of the program catches the new exception.
In this example, my_unexpected_handler's int exception is compatible with the violated specification, and main successfully catches it. But consider the variation
#include <exception>
using namespace std;
void my_unexpected_handler()
{
throw 1;
}
void f() throw(char)
{
throw 1L; // oops -- *bad* function
}
int main()
{
set_unexpected(my_unexepected_handler);
try
{
f();
}
catch (...)
{
}
return 0;
}
my_unexpected_handler still gets called after the unexpected exception, and still lobs a desperation int. Unfortunately, that int now contradicts the violated exception specification. Thus, we now have two successive violations of the same exception specification -- first by f, and now by f's alleged rescuer, my_unexpected_handler.
At this point, the program gives up and calls the Library routine terminate to self-destruct. The terminate routine is the Standard Library's last line of exception-handling defense. In circumstances where a program's exception handling is hopelessly confused or knotted, the Standard requires that program to call terminate. The Standard's Subclause 15.5.1 itemizes these terminate-inducing conditions.
Like an unexpected handler, a terminate handler can be user-defined. But unlike an unexpected handler, a terminate handler must end the program. Remember: By the time your terminate handler is entered, exception handling is already a lost cause; the last thing a program needs in that situation is for a terminate handler to toss out yet another exception.
You don't want your programs calling terminate if you can avoid it. terminate is really a glorified exit. If terminate gets called, your program is about to die an unpleasant death.
Just as Visual C++ does not properly support unexpected, so, too, does it not properly support terminate. To see this in action (or inaction), try running
#include <exception>
#include <stdlib.h>
#include <stdio.h>
using namespace std;
void my_terminate_handler()
{
printf("in my_terminate_handler\n");
abort();
}
int main()
{
set_terminate(my_terminate_handler);
throw 1; // nobody catches this
return 0;
}
According to the Standard, throwing an exception that nobody catches triggers a call to terminate (this is one of those Subclause 15.5.1 conditions I alluded to just above). As a result, the above program should produce
in my_terminate_handler
But when built and run with Visual C++, this program produces nothing.
In our unexpected example, terminate is called ultimately because of the unexpected exception thrown by f. Our unexpected_handler tries to stave off this unpleasantness with a desperation throw, but to no avail; that throw ends up causing the very problem it was meant to avoid. We need to find a way that an unexpected handler can pass control to another (presumably wiser) party without risking program termination.
Happily, the Standard provides just such a way. As we've seen, objects thrown by an unexpected handler must be allowed by the violated specification. This rule has one, um, exception: If the violated specification includes the type bad_exception, a bad_exception object replaces the unexpected handler's thrown object. Example:
#include <exception>
#include <stdio.h>
using namespace std;
void my_unexpected_handler()
{
throw 1;
}
void f() throw(char, bad_exception)
{
throw 1L; // oops -- *bad* function
}
int main()
{
set_unexpected(my_unexpected_handler);
try
{
f();
}
catch (bad_exception const &)
{
printf("caught bad_exception\n");
// ... even though such an exception was never thrown
}
return 0;
}
When built and run in a Standard-conforming implementation, this program produces
caught bad_exception
When built and run with Visual C++, the program produces nothing. As Visual C++ does not trap unexpected exceptions in the first place, it has no opportunity for a bad_exception replacement.
As in previous examples, f still violates its exception specification and my_unexpected_handler still lobs an int. The difference: f's specification now includes bad_exception. As a result, the program silently replaces my_unexpected_handler's originally thrown int with a bad_exception object. Because bad_exception is allowed by the violated specification, terminate is not called, and the bad_exception can be caught by another party.
Net effect: The original long exception thrown by f is mapped first to an int, then to a bad_exception. Not only does this mapping avoid the double jeopardy problem that previously induced terminate, it also provides an editorial to the rest of the program. The existence of a bad_exception object implies that somebody originally threw an unexpected exception. By catching such an object close to the point of offense, a program has a decent shot of robust recovery.
I also want to note a curious quirk. In the code, you see f throw a long, my_unexpected_handler throw an int, and nobody throw a bad_exception -- yet main catches a bad_exception object anyway. Yes, the program catches an object that it never threw. To my knowledge, the interaction of an unexpected handler with bad_exception is the only circumstance allowing such behavior.
The C++ Standard identifies three "special" exception handling functions. Of these, you've already seen terminate and unexpected. The last, and simplest, is uncaught_exception. From the Standard (15.5.3):
The function
bool uncaught_exception()
returns true after completing evaluation of the object to be thrown until completing the initialization of the exception-declaration in the matching handler. This includes stack unwinding. If the exception is rethrown, uncaught_exception() returns true from the point of rethrow until the rethrown exception is caught again.
uncaught_exception lets you discover if your program has thrown an exception it hasn't yet caught. The function is especially meaningful in the context of destructors:
#include <exception>
#include <stdio.h>
using namespace std;
class X
{
public:
~X();
};
X::~X()
{
if (uncaught_exception())
printf("X::~X called during stack unwind\n");
else
printf("X::~X called normally\n");
}
int main()
{
X x1;
try
{
X x2;
throw 1;
}
catch (...)
{
}
return 0;
}
Under a Standard-conforming implementation, this program yields
X::~X called during stack unwind
X::~X called normally
Both x1 and x2 are constructed before main throws its exception. The resulting stack unwind calls x2's destructor. Since an uncaught exception is active during that destructor call, uncaught_exception returns true. By the time x1's destructor is called (when main exits), the exception has been defused and uncaught_exception returns false.
As before, Visual C++ does not support the Standard here. Built with that environment, the program yields
X::~X called normally
X::~X called normally
If you are inclined toward Microsoft's Structured Exception Handling (SEH), which I surveyed in Part 2 of this series, know that uncaught_exception is analogous to SEH's AbnormalTermination. Within their respective domains, each function determines if a thrown exception is active and not yet handled.
Most functions don't throw exceptions directly, but instead percolate exceptions thrown by others. Determining which exceptions a function may percolate can be quite tricky, especially when those exceptions come from other functions that lack exception specifications. bad_exception offers a safety valve, a way to protect against exception contexts you can't fully analyze.
This protection works -- but, like exception handling in general, requires you to explicitly design around it. For each function that might violate its exception specification, you must remember to add bad_exception to that specification, then catch the bad_exception somewhere meaningful. bad_exception is no different from any other exception: If you aren't prepared to handle it, you do yourself no favors by generating it. An uncaught bad_exception causes a program to terminate, just as if you'd never bothered with bad_exception in the first place.
An exception specification makes your intent explicit. It says "Here is the set of exceptions I allow this function to throw; if the function throws anything else, either my design is bad or the program is buggy." An unexpected exception, no matter how it manifests, represents a logic error. I suggest that you are better off having such an error occur in a predictable and bounded way.
All of this implies that you can profile your code's exception behavior in the first place. Unfortunately, such profiling can border on sorcery. Next time, I'll give you some guidelines for analyzing the exception domain within your code.
Robert Schmidt is a technical writer for MSDN. His other major writing distraction is the C/C++ Users Journal, for which he is a contributing editor and columnist. In previous career incarnations he's been a radio DJ, wild-animal curator, astronomer, pool-hall operator, private investigator, newspaper carrier, and college tutor.