Lesson #21

Various Other Exception Topics

Overview

In this lesson I will cover the following topics

  1. Catching and rethrowing exceptions.

  2. Catching all exceptions.

  3. Exception specifications.

Body

Rethrowing

Normally, when an exception is caught and the handler finishes executing, the program continues with the code following the handler. Thus, catching an exception effectively ends the process of stack unwinding. If an exception is caught in function f, for example, then as far as the callers of f are concerned there was no exception. What this means is that a function should only catch an exception if it is prepared to "absorb" the effects of the exception. In other words, by catching an exception, the function is saying that the exception does not mean it has failed to do its work.

Suppose, for example, that a particular function is in a position to possibly correct the problem that caused the exception. That function might want to retry the operation that failed after taking corrective action. Here is one way that might look:

void f( )
{
    int retry_count = 3;

    while( retry_count != 0 ) {
        try {
            // Set up and try a complex operation.
            break;
        }
        catch( exception &bad_thing ) {
            // Take corrective action.
            retry_count--;
        }
    }

    if( retry_count == 0 )
        throw exception( "function failed" );
}

Here I'm catching and throwing std::exception but in real life more specific exception types would probably be more appropriate. In fact, it would be common for this function to throw a different kind of exception than it is catching. It would catch exceptions specific to the operations it is attempting in the loop while it would throw an exception of its own type to represent its failure. In any event, the example above depends on the fact that the while loop continues to execute after the handler is finished. If the operation finally completes successfully, the function does not throw an exception and its caller never knows the difference.

It is also possible to throw exceptions inside an exception handler. This is useful if you want to translate exceptions from one type to another. For example:

void f( )
{
    try {
        // Do whatever.
    }
    catch( NetworkError &bad_thing ) {
        throw MyErrorType( "I failed to do whatever" );
    }
}

In this way, the caller of function f does not have to know anything about the lower level exceptions. As far as the caller of f is concerned the function f either works or it throws a MyErrorType exception. Note that if you do a lot of computations in your exception handlers it is entirely possible that one of the operations you do in the handler might throw an exception of its own. In practice, this isn't usually a major concern because most of what exception handlers do is similar to what destructors do: return resources to the system. Generally, such operations don't throw.

In some cases, you may find it desirable to catch an exception, do some clean-up operations, and then pass the same exception you caught on to a higher level function. This can be done by just using the throw keyword by itself in a handler. For example:

void f( )
{
    try {
        // Do whatever.
    }
    catch( NetworkError &bad_thing ) {
        // Do some special clean up, logging, or other such thing.
        throw;
    }
}

Here the throw causes the NetworkError exception that was caught to be thrown again allowing some higher level exception handler to further process it. Notice that if the actual exception caught was derived from NetworkError, the exception that would be rethrown by this example would be the full exception originally thrown and not just the NetworkError sub-object of that exception.

This facility to rethrow a caught exception is not as necessary as you might think. Since local objects are destroyed automatically during the stack unwinding process, most clean-up activities that you might want to do can be handled by simply putting them in the destructors of suitable objects. However, the rethrowing feature is useful in cases where you can't do that (for example, if you are calling functions in a legacy C library to get and release resources) or when you want special things done only when an exception occurs (such as logging the failure) but aren't in a position to fully handle the exception.

Catching all exceptions

In some situations, you will want to catch all exceptions regardless of their type. For instance, some of my examples above would make more sense if every exception thrown by the lower level code was caught and processed by the example. There are other cases where catching all exceptions is important. For example, in a call-back function where the caller is not even part of the same program, it is usually important that no C++ exception attempt to propagate beyond the C++ code. Also, if an exception attempts to propagate beyond main the program will be terminated. In some cases, that is not acceptable.

If all exceptions used by the program and all the libraries it calls were derived from std::exception, it would be possible to catch all exceptions by just catching that base class. However, C++ does allow any type to be used for exceptions. In general, you can't be sure that all the code in your program is using the standard exception hierarchy. Thus, the most general way to deal with all exceptions in one handler is the use the special ... construct. Here is one way to make a main function that won't terminate if any exception reaches it.

int main( int argc, char **argv )
{
    while( true ) {
        try {
            my_main( argc, argv );
        }
        catch( ... ) {
            cout << "Unexpected exception reached main. Restarting..." << endl;
        }
    }
}

Here I'm assuming the actual main function of the program is called my_main. The main function that is used as the program's entry point is just a wrapper around my_main that deals with any exceptions my_main might produce. I'm assuming that my_main goes into an infinite loop and that this program is never supposed to terminate. If an exception occurs during the execution of my_main, the catch-all handler in main will catch it, and the program will loop back and simply re-execute itself. The ability to restart a program from scratch even if there are logic errors in it is one reason why some people feel that logic errors should be reported with exceptions and not handled by assertions.

Notice that in the catch-all handler I do not know the type of the exception object caught. As a result, I cannot do any operations on that object. However, in many cases, that is not a problem. If necessary, I can rethrow the exception. Here is an example from my own programming.

//
// void WorkQ<T>::pop( T &outgoing )
//
// This function pops an object of type T. It does it in such a way that
// there will be no races inside the queue. If there is nothing in the
// queue, the thread will be blocked until something appears. If an
// exception is thrown when the object is copied out of the queue this
// function leaves the object on the queue (so that a future pop can try
// again). In that case, the queue is properly unlocked and the counting
// semaphores are set to appropriate states.
//
template<typename T>
void WorkQ<T>::pop( T &outgoing )
{
    used_slots.down( );
    {
        mutex_sem::grabber critical( mutex );
        try {
            outgoing = the_queue.front( );
            the_queue.pop( );
        }
        catch( ... ) {
            used_slots.up( );
            throw;
        }
    }
    free_slots.up( );
}

This code is part of a template I wrote for managing a queue of objects in a multithreaded environment. The type of objects in the queue is given by T. The method above removes an item from the queue and puts it into the object referenced by outgoing. The problem is: what happens if copying the object from the queue to outgoing fails with an exception? In that case, I need to modify the count of objects in the queue to record the fact that the object was not copied as expected. Since I have no idea what type of exception copying an object of type T might throw (std::bad_alloc is a good possibility but there might be others), the best I can really do is to catch all exceptions. However, it's not my place here to handle an exception from the copying. Depending on the kind of exception that occurred, the proper response might be very different. The best I can do here is to be sure my queue is in order and ready for a possible retry of the operation. I don't want to lose any information if an exception occurs.

The queue also needs to be unlocked when this method completes. Since I want that done both when an exception happens and when it doesn't, I put the locking and unlocking operations into the constructor and destructor of a suitable object. In this case, it's a mutex_sem::grabber object named critical. Notice how I use an extra pair of braces in the method to delimit the region of code where I want the locking to be active. The destructor of critical will execute when the extra closing brace is reached because that is where critical goes out of scope. The destructor will also be executed should an exception be thrown out of this code, exactly as desired.

Notice also that I'm implicitly assuming that the_queue.pop( ) won't throw an exception. If the exception happens after I've successfully copied the object out, keeping the object on the queue is not entirely appropriate. However, since the_queue.pop() involves the destruction of the object on the queue, it is unlikely to throw (remember that you can generally assume destructors don't throw).

Exception specifications

This feature was deprecated in C++ 2011 and removed from the language entirely in C++ 2017. Even when writing C++ 1998 code, it is recommended that you do not use exception specifications except for the no-throw specification. Starting with C++ 2011 a special noexcept declaration can be used in place of no-throw specifications.

The example above also shows, once again, the importance of knowing which functions might throw what exceptions. It turns out that C++ has a mechanism that allows the programmer to formally specify which exceptions a function might throw. This mechanism, called exception specifications, is one of the more controversial aspects of the C++ standard.

Consider the three declarations for the function f below.

void f( ) throw( );
void f( ) throw( NetworkError );
void f( ) throw( ConnectError, BadIpError );

The first declares f as throwing no exceptions at all. The second declares f as throwing NetworkError exceptions, or any exception derived from NetworkError. The third declares f as throwing either ConnectError or BadIpError (or anything derived from those classes). Of course, the function may execute without throwing any exceptions. However, the exception specification lists the only exceptions that a function is allowed to throw. The function will not throw any exception other than those named in its exception specification.

In a real program, you would not see the three different declarations of f as I show above. The compiler insures that all declarations of a function have exception specifications that agree. Of course, there might be several different functions named f that are overloaded and each of those might have different exception specifications. Keep in mind that overloaded functions are different functions.

The critical question is: what interpretation does one give to a function declaration that does not have an exception specification? In other words, what is the meaning of

void f( );

It might have been defined to mean that f throws no exceptions. In other words, no exception specification would be the same as an empty exception specification. Furthermore, the compiler might have been required to check the exception specifications of all the functions f calls and verify that either all exceptions that might be thrown by those functions are caught or are mentioned in f's exception specification as well. In such a world, the compiler would be able to prove, at compile time, that every possible exception was accounted for in some way. This would help ensure that error conditions are not inadvertently ignored by a program—a problem that is very common with many programs today.

However, this is not that world. Exception specifications were added to C++ rather late in the standardization process. Many millions of lines of code had been written without exception specifications for the simple reason that the feature did not exist at the time the code was written. Yet this code either threw exceptions or it used libraries that threw exceptions. For this reason, the C++ standard states that a function without an exception specification might throw any exception. You need to use an empty exception specification to explicitly state that a function throws nothing.

Because the lack of an exception specification implies that any exception might be thrown, it is now impractical for the compiler to enforce exception specifications. If a function f calls a function g and if g has no exception specification, f either would have to either catch every possible exception or say that it, too, might throw any exception. Since functions without exception specifications are common, this would essentially make exception specifications totally useless.

Thus, in C++ exception specifications are enforced not by the compiler, but at run time. This implies that when an exception is thrown there is some extra run time checking done at each function to ensure that the exception is compatible with that function's exception specification. While this checking does take some extra time, it is not normally a problem because exceptions should be rare. In the normal case, when there is no exception, there does not need to be any extra checking.

What happens if a function tries to throw an exception that is not compatible with that function's exception specification? The program is immediately aborted! This may seem extremely harsh (many people think so), but throwing an unexpected exception is a type of logic error and thus aborting the program is not necessarily unreasonable. Remember that the C library's assert macro also immediately aborts the program when an assertion fails. The reasoning is the same: such an error should only occur during development. When it happens, it makes sense to kill the program at once so that the exact location of the bug is easier to find (the developer would use a debugger to capture the program's state right as it aborts).

Actually, the full story is a bit more complicated. When a function attempts to throw an unexpected exception, the C++ run time system will call a special function in the library called unexpected. That function aborts the program by default, but the exact behavior can be changed by the programmer. Here is one way it might look.

#include <exception>

// This function is what I want to happen if some function throws an
// unexpected exception. This function can't return normally. It must
// either abort (usually by calling std::terminate( ) ) or throw an
// exception of its own.
//
void my_unexpected_handler( )
{
    // Perhaps log that an unexpected exception occurred?
    throw std::bad_exception;
}

// In main I will initialize the program. This includes setting up my
// unexpected handler.
//
int main( int argc, char **argv )
{
    // When an unexpected exception occurs, call my handler.
    std::set_unexpected( my_unexpected_handler );

    // Proceed with the program...
}

// Here is a typical function. If it throws an unexpected exception,
// my unexpected handler will be invoked.
//
void f( ) throw( network_error, std::bad_exception )
{
    // Do stuff.
}

In this example, I define a function that I want executed whenever any function encounters an unexpected exception. This function is completely general and can do whatever it likes (for example, it might log the error or perform some clean-up activities). However, the unexpected handler can't return in the normal way. It can either abort the program itself, usually be calling std::terminate, or it can throw an exception of its own. If the unexpected handler does throw an exception, that exception must satisfy the exception specification of the function that originally got the unexpected exception. By throwing a new exception, the unexpected handler can essentially translate unexpected exceptions into some type of expected exception. This new exception then propagates up the call stack in the usual way, letting the destructors of local objects perform their usual clean-up duties.

It is commonly felt that this scheme is not all that useful in real life. The problem is that translating unexpected exceptions essentially throws away information about the actual error with little gained in return. Also, exception specifications as they are currently defined do not play well with certain other C++ features (specifically templates) or with call back functions. Despite that, I believe that exception specifications can be useful in two contexts.

Summary

  1. It is possible to rethrow the exception that was last caught by just using a throw statement by itself. You sometimes need to do this if you are writing a function that needs to "intercept" an exception to either translate it to something else or to do some local work before passing the exception on.

  2. Using ... in an exception handler causes that handler to catch all exceptions. In programs that are designed to run forever it is common to find such handlers in main where they prevent an unplanned exception from terminating the entire application.

  3. You can list the exceptions a function might throw in the declaration of that function using an exception specification. For compatibility with older code, functions without an exception specification are effectively declared to throw any kind of exception whatever. Exception specifications are not enforced at compile time but rather at run time. If a function attempts to throw an exception that is not listed in its exception specification, the program is immediately aborted. However, it is possible to configure your program to perform some other action when an unexpected exception occurs.

  4. Exception specifications are controversial and probably should be avoided. A case can be made for using them (like C's assert macro) as a testing tool during development. A case can also be made for using no-throw specifications on functions that are not intended to throw any exceptions.

© Copyright 2023 by Peter Chapin.
Last Revised: August 7, 2023