Chapter 10: Exceptions

C supports several ways for a program to react to situations breaking the normal unhampered flow of a program: In C++ all these flow-breaking methods are still available. However, of the mentioned alternatives, setjmp and longjmp isn't frequently encountered in C++ (or even in C) programs, due to the fact that the program flow is completely disrupted.

C++ offers exceptions as the preferred alternative to, e.g., setjmp and longjmp. Exceptions allow C++ programs to perform a controlled non-local return, without the disadvantages of longjmp and setjmp.

Exceptions are the proper way to bail out of a situation which cannot be handled easily by a function itself, but which is not disastrous enough for a program to terminate completely. Also, exceptions provide a flexible layer of control between the short-range return and the crude exit.

In this chapter exceptions are covered. First an example is given of the different impact exceptions and the setjmp/longjmp combination have on programs. This example is followed by a discussion of the formal aspects of exceptions. In this part the guarantees our software should be able to offer when confronted with exceptions are presented. Exceptions and their guarantees have consequences for constructors and destructors. We'll encounter these consequences at the end of this chapter.

10.1: Exception syntax

Before contrasting the traditional C way of handling non-local gotos with exceptions let's introduce the syntactic elements that are involved when using exceptions.

10.2: An example using exceptions

In the following examples the same basic program is used. The program uses two classes, Outer and Inner.

First, an Outer object is defined in main, and its member Outer::fun is called. Then, in Outer::fun an Inner object is defined. Having defined the Inner object, its member Inner::fun is called.

That's about it. The function Outer::fun terminates calling inner's destructor. Then the program terminates, activating outer's destructor. Here is the basic program:

    #include <iostream>
    using namespace std;

    class Inner
    {
        public:
            Inner();
            ~Inner();
            void fun();
    };
    Inner::Inner()
    {
        cout << "Inner constructor\n";
    }
    Inner::~Inner()
    {
        cout << "Inner destructor\n";
    }
    void Inner::fun()
    {
        cout << "Inner fun\n";
    }

    class Outer
    {
        public:
            Outer();
            ~Outer();
            void fun();
    };
    Outer::Outer()
    {
        cout << "Outer constructor\n";
    }
    Outer::~Outer()
    {
        cout << "Outer destructor\n";
    }
    void Outer::fun()
    {
        Inner in;
        cout << "Outer fun\n";
        in.fun();
    }

    int main()
    {
        Outer out;
        out.fun();
    }

    /*
        Generated output:
    Outer constructor
    Inner constructor
    Outer fun
    Inner fun
    Inner destructor
    Outer destructor
    */
After compiling and running, the program's output is entirely as expected: the destructors are called in their correct order (reversing the calling sequence of the constructors).

Now let's focus our attention on two variants in which we simulate a non-fatal disastrous event in the Inner::fun function. This event must supposedly be handled near main's end.

We'll consider two variants. In the first variant the event is handled by setjmp and longjmp; in the second variant the event is handled using C++'s exception mechanism.

10.2.1: Anachronisms: `setjmp' and `longjmp'

The basic program from the previous section is slightly modified to contain a variable jmp_buf jmpBuf used by setjmp and longjmp.

The function Inner::fun calls longjmp, simulating a disastrous event, to be handled near main's end. In main a target location for the long jump is defined through the function setjmp. Setjmp's zero return indicates the initialization of the jmp_buf variable, in which case Outer::fun is called. This situation represents the `normal flow'.

The program's return value is zero only if Outer::fun terminates normally. The program, however, is designed in such a way that this won't happen: Inner::fun calls longjmp. As a result the execution flow returns to the setjmp function. In this case it does not return a zero return value. Consequently, after calling Inner::fun from Outer::fun main's if-statement is entered and the program terminates with return value 1. Try to follow these steps when studying the following program source, which is a direct modification of the basic program given in section 10.2:

    #include <iostream>
    #include <setjmp.h>
    #include <cstdlib>

    using namespace std;

    jmp_buf jmpBuf;

    class Inner
    {
        public:
            Inner();
            ~Inner();
            void fun();
    };

    Inner::Inner()
    {
        cout << "Inner constructor\n";
    }
    void Inner::fun()
    {
        cout << "Inner fun\n";
        longjmp(jmpBuf, 0);
    }
    Inner::~Inner()
    {
        cout << "Inner destructor\n";
    }

    class Outer
    {
        public:
            Outer();
            ~Outer();
            void fun();
    };

    Outer::Outer()
    {
        cout << "Outer constructor\n";
    }
    Outer::~Outer()
    {
        cout << "Outer destructor\n";
    }
    void Outer::fun()
    {
        Inner in;
        cout << "Outer fun\n";
        in.fun();
    }

    int main()
    {
        Outer out;

        if (setjmp(jmpBuf) != 0)
            return 1;

        out.fun();
    }
    /*
        Generated output:
    Outer constructor
    Inner constructor
    Outer fun
    Inner fun
    Outer destructor
    */
This program's output clearly shows that inner's destructor is not called. This is a direct consequence of the non-local jump performed by longjmp. Processing proceeds immediately from the longjmp call inside Inner::fun to setjmp in main. There, its return value is unequal zero, and the program terminates with return value 1. Because of the non-local jump Inner::~Inner is never executed: upon return to main's setjmp the existing stack is simply broken down disregarding any destructors waiting to be called.

This example illustrates that the destructors of objects can easily be skipped when longjmp and setjmp are used and C++ programs should therefore avoid those functions like the plague.

10.2.2: Exceptions: the preferred alternative

Exceptions are C++'s answer to the problems caused by setjmp and longjmp. Here is an example using exceptions. The program is once again derived from the basic program of section 10.2:
    #include <iostream>
    using namespace std;

    class Inner
    {
        public:
            Inner();
            ~Inner();
            void fun();
    };
    Inner::Inner()
    {
        cout << "Inner constructor\n";
    }
    Inner::~Inner()
    {
        cout << "Inner destructor\n";
    }
    void Inner::fun()
    {
        cout << "Inner fun\n";
        throw 1;
        cout << "This statement is not executed\n";
    }

    class Outer
    {
        public:
            Outer();
            ~Outer();
            void fun();
    };

    Outer::Outer()
    {
        cout << "Outer constructor\n";
    }
    Outer::~Outer()
    {
        cout << "Outer destructor\n";
    }
    void Outer::fun()
    {
        Inner in;
        cout << "Outer fun\n";
        in.fun();
    }

    int main()
    {
        Outer out;
        try
        {
            out.fun();
        }
        catch (int x)
        {}
    }
    /*
        Generated output:
    Outer constructor
    Inner constructor
    Outer fun
    Inner fun
    Inner destructor
    Outer destructor
    */
Inner::fun now throws an int exception where a longjmp was previously used. Since in.fun is called by out.fun, the exception is generated within the try block surrounding the out.fun call. As an int value was thrown this value reappears in the catch clause beyond the try block.

Now Inner::fun terminates by throwing an exception instead of calling longjmp. The exception is caught in main, and the program terminates. Now we see that inner's destructor is properly called. It is interesting to note that Inner::fun's execution really terminates at the throw statement: The cout statement, placed just beyond the throw statement, isn't executed.

What did this example teach us?

10.3: Throwing exceptions

Exceptions are generated by throw statements. The throw keyword is followed by an expression, defining the thrown exception value. Example:
    throw "Hello world";        // throws a char *
    throw 18;                   // throws an int
    throw string{ "hello" };    // throws a string

Local objects cease to exist when a function terminates. This is no different for exceptions.

Objects defined locally in functions are automatically destroyed once exceptions thrown by these functions leave these functions. This also happens to objects thrown as exceptions. However, just before leaving the function context the object is copied and it is this copy that eventually reaches the appropriate catch clause.

The following examples illustrates this process. Object::fun defines a local Object toThrow, that is thrown as an exception. The exception is caught in main. But by then the object originally thrown doesn't exist anymore, and main received a copy:

    #include <iostream>
    #include <string>
    using namespace std;

    class Object
    {
        string d_name;

        public:
            Object(string name)
            :
                d_name(name)
            {
                cout << "Constructor of " << d_name << "\n";
            }
            Object(Object const &other)
            :
                d_name(other.d_name + " (copy)")
            {
                cout << "Copy constructor for " << d_name << "\n";
            }
            ~Object()
            {
                cout << "Destructor of " << d_name << "\n";
            }
            void fun()
            {
                Object toThrow("'local object'");
                cout << "Calling fun of " << d_name << "\n";
                throw toThrow;
            }
            void hello()
            {
                cout << "Hello by " << d_name << "\n";
            }
    };

    int main()
    {
        Object out{ "'main object'" };
        try
        {
            out.fun();
        }
        catch (Object o)
        {
            cout << "Caught exception\n";
            o.hello();
        }
    }
Object's copy constructor is special in that it defines its name as the other object's name to which the string " (copy)" is appended. This allow us to monitor the construction and destruction of objects more closely. Object::fun generates an exception, and throws its locally defined object. Just before throwing the exception the program has produced the following output:
    Constructor of 'main object'
    Constructor of 'local object'
    Calling fun of 'main object'

When the exception is generated the next line of output is produced:

    Copy constructor for 'local object' (copy)

The local object is passed to throw where it is treated as a value argument, creating a copy of toThrow. This copy is thrown as the exception, and the local toThrow object ceases to exist. The thrown exception is now caught by the catch clause, defining an Object value parameter. Since this is a value parameter yet another copy is created. Thus, the program writes the following text:

    Destructor of 'local object'
    Copy constructor for 'local object' (copy) (copy)

The catch block now displays:

    Caught exception

Following this o's hello member is called, showing us that we indeed received a copy of the copy of the original toThrow object:

    Hello by 'local object' (copy) (copy)

Then the program terminates and its remaining objects are now destroyed, reversing their order of creation:

    Destructor of 'local object' (copy) (copy)
    Destructor of 'local object' (copy)
    Destructor of 'main object'

The copy created by the catch clause clearly is superfluous. It can be avoided by defining object reference parameters in catch clauses: `catch (Object &o)'. The program now produces the following output:

    Constructor of 'main object'
    Constructor of 'local object'
    Calling fun of 'main object'
    Copy constructor for 'local object' (copy)
    Destructor of 'local object'
    Caught exception
    Hello by 'local object' (copy)
    Destructor of 'local object' (copy)
    Destructor of 'main object'

Only a single copy of toThrow was created.

It's a bad idea to throw a pointer to a locally defined object. The pointer is thrown, but the object to which the pointer refers ceases to exist once the exception is thrown. The catcher receives a wild pointer. Bad news....

Let's summarize the above findings:

Exceptions are thrown in situations where a function can't complete its assigned task, but the program is still able to continue. Imagine a program offering an interactive calculator. The program expects numeric expressions, which are evaluated. Expressions may show syntactic errors or it may be mathematically impossible to evaluate them. Maybe the calculator allows us to define and use variables and the user might refer to non-existing variables: plenty of reasons for the expression evaluation to fail, and so many reasons for exceptions to be thrown. None of those should terminate the program. Instead, the program's user is informed about the nature of the problem and is invited to enter another expression. Example:
    if (!parse(expressionBuffer))           // parsing failed
        throw "Syntax error in expression";

    if (!lookup(variableName))              // variable not found
        throw "Variable not defined";

    if (divisionByZero())                   // unable to do division
        throw "Division by zero is not defined";

Where these throw statements are located is irrelevant: they may be found deeply nested inside the program, or at a more superficial level. Furthermore, functions may be used to generate the exception to be thrown. An Exception object might support stream-like insertion operations allowing us to do, e.g.,

    if (!lookup(variableName))
        throw Exception() << "Undefined variable '" << variableName << "';

10.3.1: The empty `throw' statement

Sometimes it is required to inspect a thrown exception. An exception catcher may decide to ignore the exception, to process the exception, to rethrow it after inspection or to change it into another kind of exception. For example, in a server-client application the client may submit requests to the server by entering them into a queue. Normally every request is eventually answered by the server. The server may reply that the request was successfully processed, or that some sort of error has occurred. On the other hand, the server may have died, and the client should be able to discover this calamity, by not waiting indefinitely for the server to reply.

In this situation an intermediate exception handler is called for. A thrown exception is first inspected at the middle level. If possible it is processed there. If it is not possible to process the exception at the middle level, it is passed on, unaltered, to a more superficial level, where the really tough exceptions are handled.

By placing an empty throw statement in the exception handler's code the received exception is passed on to the next level that might be able to process that particular type of exception. The rethrown exception is never handled by one of its neighboring exception handlers; it is always transferred to an exception handler at a more superficial level.

In our server-client situation a function

    initialExceptionHandler(string &exception)

could be designed to handle the string exception. The received message is inspected. If it's a simple message it's processed, otherwise the exception is passed on to an outer level. In initialExceptionHandler's implementation the empty throw statement is used:

    void initialExceptionHandler(string &exception)
    {
        if (!plainMessage(exception))
            throw;

        handleTheMessage(exception);
    }

Below (section 10.5), the empty throw statement is used to pass on the exception received by a catch-block. Therefore, a function like initialExceptionHandler can be used for a variety of thrown exceptions, as long as their types match initialExceptionHandler's parameter, which is a string.

The next example jumps slightly ahead, using some of the topics covered in chapter 14. The example may be skipped, though, without loss of continuity.

A basic exception handling class can be constructed from which specific exception types are derived. Suppose we have a class Exception, having a member function ExceptionType Exception::severity. This member function tells us (little wonder!) the severity of a thrown exception. It might be Info, Notice, Warning, Error or Fatal. The information contained in the exception depends on its severity and is processed by a function handle. In addition, all exceptions support a member function like textMsg, returning textual information about the exception in a string.

By defining a polymorphic function handle it can be made to behave differently, depending on the nature of a thrown exception, when called from a basic Exception pointer or reference.

In this case, a program may throw any of these five exception types. Assuming that the classes Message and Warning were derived from the class Exception, then the handle function matching the exception type will automatically be called by the following exception catcher:

    //
    catch(Exception &ex)
    {
        cout << e.textMsg() << '\n';

        if
        (
            ex.severity() != ExceptionType::Warning
            &&
            ex.severity() != ExceptionType::Message
        )
            throw;              // Pass on other types of Exceptions

        ex.handle();            // Process a message or a warning
    }

Now anywhere in the try block preceding the exception handler Exception objects or objects of one of its derived classes may be thrown. All those exceptions will be caught by the above handler. E.g.,

    throw Info{};
    throw Warning{};
    throw Notice{};
    throw Error{};
    throw Fatal{};

10.4: The try block

The try-block surrounds throw statements. Remember that a program is always surrounded by a global try block, so throw statements may appear anywhere in your code. More often, though, throw statements are used in function bodies and such functions may be called from within try blocks.

A try block is defined by the keyword try followed by a compound statement. This block, in turn, must be followed by at least one catch handler:

    try
    {
                // any statements here
    }
    catch(...)  // at least one catch clause here
    {}

Try-blocks are commonly nested, creating exception levels. For example, main's code is surrounded by a try-block, forming an outer level handling exceptions. Within main's try-block functions are called which may also contain try-blocks, forming the next exception level. As we have seen (section 10.3.1), exceptions thrown in inner level try-blocks may or may not be processed at that level. By placing an empty throw statement in an exception handler, the thrown exception is passed on to the next (outer) level.

10.5: Catching exceptions

A catch clause consists of the keyword catch followed by a parameter list defining one parameter specifying type and (parameter) name of the exception caught by that particular catch handler. This name may then be used as a variable in the compound statement following the catch clause. Example:
    catch (string &message)
    {
        // code to handle the message
    }

Primitive types and objects may be thrown as exceptions. It's a bad idea to throw a pointer or reference to a local object, but a pointer to a dynamically allocated object may be thrown if the exception handler deletes the allocated memory to prevent a memory leak. Nevertheless, throwing such a pointer is dangerous as the exception handler won't be able to distinguish dynamically allocated memory from non-dynamically allocated memory, as illustrated by the next example:

    try
    {
        static int x;
        int *xp = &x;

        if (condition1)
            throw xp;

        xp = new int(0);
        if (condition2)
            throw xp;
    }
    catch (int *ptr)
    {
        // delete ptr or not?
    }

Close attention should be paid to the nature of the parameter of the exception handler, to make sure that when pointers to dynamically allocated memory are thrown the memory is returned once the handler has processed the pointer. In general pointers should not be thrown as exceptions. If dynamically allocated memory must be passed to an exception handler then the pointer should be wrapped in a smart pointer, like unique_ptr or shared_ptr (cf. sections 18.3 and 18.4).

Multiple catch handlers may follow a try block, each handler defining its own exception type. The order of the exception handlers is important. When an exception is thrown, the first exception handler matching the type of the thrown exception is used and remaining exception handlers are ignored. Eventually at most one exception handler following a try-block is activated. Normally this is of no concern as each exception has its own unique type.

Example: if exception handlers are defined for char *s and void *s then NTBSs are caught by the former handler. Note that a char * can also be considered a void *, but the exception type matching procedure is smart enough to use the char * handler with the thrown NTBS. Handlers should be designed very type specific to catch the correspondingly typed exception. For example, int-exceptions are not caught by double-catchers, char-exceptions are not caught by int-catchers. Here is a little example illustrating that the order of the catchers is not important for types not having any hierarchal relationship to each other (i.e., int is not derived from double; string is not derived from an NTBS):

#include <iostream>
using namespace std;

int main()
{
    while (true)
    {
        try
        {
            string s;
            cout << "Enter a,c,i,s for ascii-z, char, int, string "
                                                      "exception\n";
            getline(cin, s);
            switch (s[0])
            {
                case 'a':
                    throw "ascii-z";
                case 'c':
                    throw 'c';
                case 'i':
                    throw 12;
                case 's':
                    throw string{};
            }
        }
        catch (string const &)
        {
            cout << "string caught\n";
        }
        catch (char const *)
        {
            cout << "ASCII-Z string caught\n";
        }
        catch (double)
        {
            cout << "isn't caught at all\n";
        }
        catch (int)
        {
            cout << "int caught\n";
        }
        catch (char)
        {
            cout << "char caught\n";
        }
    }
}
Rather than defining specific exception handlers a specific class can be designed whose objects contain information about the exception. Such an approach was mentioned earlier, in section 10.3.1. Using this approach, there's only one handler required, since we know we don't throw other types of exceptions:
    try
    {
        // code throws only Exception objects
    }
    catch (Exception &ex)
    {
        ex.handle();
    }

When the code of an exception handler has been processed, execution continues beyond the last exception handler directly following the matching try-block (assuming the handler doesn't itself use flow control statements (like return or throw) to break the default flow of execution). The following cases can be distinguished:

All statements in a try block following an executed throw-statement are ignored. However, objects that were successfully constructed within the try block before executing the throw statement are destroyed before any exception handler's code is executed.

10.5.1: The default catcher

At a certain level of the program only a limited set of handlers may actually be required. Exceptions whose types belong to that limited set are processed, all other exceptions are passed on to exception handlers of an outer level try block.

An intermediate type of exception handling may be implemented using the default exception handler, which must be (due to the hierarchal nature of exception catchers, discussed in section 10.5) placed beyond all other, more specific exception handlers.

This default exception handler cannot determine the actual type of the thrown exception and cannot determine the exception's value but it may execute some statements, and thus do some default processing. Moreover, the caught exception is not lost, and the default exception handler may use the empty throw statement (see section 10.3.1) to pass the exception on to an outer level, where it's actually processed. Here is an example showing this use of a default exception handler:

    #include <iostream>
    using namespace std;

    int main()
    {
        try
        {
            try
            {
                throw 12.25;    // no specific handler for doubles
            }
            catch (int value)
            {
                cout << "Inner level: caught int\n";
            }
            catch (...)
            {
                cout << "Inner level: generic handling of exceptions\n";
                throw;
            }
        }
        catch(double d)
        {
            cout << "Outer level may use the thrown double: " << d << '\n';
        }
    }
    /*
        Generated output:
    Inner level: generic handling of exceptions
    Outer level may use the thrown double: 12.25
    */
The program's output illustrates that an empty throw statement in a default exception handler throws the received exception to the next (outer) level of exception catchers, keeping type and value of the thrown exception.

Thus, basic or generic exception handling can be accomplished at an inner level, while specific handling, based on the type of the thrown expression, can be provided at an outer level. Additionally, particularly in multi-threaded programs (cf. chapter 20), thrown exceptions can be transferred between threads after converting std::exception objects to std::exception_ptr objects. This proceduce can even be used from inside the default catcher. Refer to section 20.12.1 for further coverage of the class std::exception_ptr.

10.6: Functions that cannot throw exceptions: the `noexcept' keyword

Once a function has been defined it's often called from other functions. If called functions are not defined in the same source file as calling functions the called functions must be declared, for which header files are often used. Those called functions might throw exceptions, which might be unacceptible to the function calling those other functions. E.g., functions like swap and destructors may not throw exceptions.

Functions that may not throw exceptions can be declared and defined by specifying the noexcept keyword (see section 10.9 for examples of function declarations specifying noexcept).

When using noecept there's a slight run-time overhead penalty because the function needs an over-all try-catch block catching any eception that might be thrown by its (called) code. When an exception is caught (violating the noexcept specification) then the catch clause calls std::terminate, ending the program.

In addition to using a plain noexcept, it can also be given an argument that is evaluated compile-time (e.g., void fun() noexcept(sizeof(int) == 4)): if the evaluation returns true then the noexcept requirement is used; if the evaluation returns false, then the noexcept requirement is ignored. Examples of this advanced use of noexcept are provided in section 23.8.

10.7: Iostreams and exceptions

The C++ I/O library was used well before exceptions were available in C++. Hence, normally the classes of the iostream library do not throw exceptions. However, it is possible to modify that behavior using the ios::exceptions member function. This function has two overloaded versions: In the I/O library, exceptions are objects of the class ios::failure, derived from ios::exception. A std::string const &message may be specified when defining a failure object. Its message may then be retrieved using its virtual char const *what() const member.

Exceptions should be used in exceptional circumstances. Therefore, we think it is questionable to have stream objects throw exceptions for fairly normal situations like EOF. Using exceptions to handle input errors might be defensible (e.g., in situations where input errors should not occur and imply a corrupted file) but often aborting the program with an appropriate error message would probably be the more appropriate action. As an example consider the following interactive program using exceptions to catch incorrect input:

    #include <iostream>
    #include <climits>
    using namespace::std;

    int main()
    {
        cin.exceptions(ios::failbit);   // throw exception on fail
        while (true)
        {
            try
            {
                cout << "enter a number: ";
                int value;
                cin >> value;
                cout << "you entered " << value << '\n';
            }
            catch (ios::failure const &problem)
            {
                cout << problem.what() << '\n';
                cin.clear();
                cin.ignore(INT_MAX, '\n');  // ignore the faulty line
            }
        }
    }

By default, exceptions raised from within ostream objects are caught by these objects, which set their ios::badbit as a result. See also the paragraph on this issue in section 14.8.

10.8: Standard exceptions

All data types may be thrown as exceptions. Several additional exception classes are now defined by the C++ standard. Before using those additional exception classes the <stdexcept> header file must be included.

All of these standard exceptions are class types by themselves, but also offer all facilities of the std::exception class and objects of the standard exception classes may also be considered objects of the std::exception class.

The std::exception class offers the member

    char const *what() const;

describing in a short textual message the nature of the exception.

C++ defines the following standard exception classes:

All additional exception classes were derived from std::exception. The constructors of all these additional classes accept std::string const & arguments summarizing the reason for the exception (retrieved by the exception::what member). The additionally defined exception classes are:

10.8.1: Standard exceptions: to use or not to use?

Since values of any type may be thrown as exceptions, you may wonder when to throw values of standard exception types and (if ever) when to throw values of other types.

Current practice in the C++ community is to throw exceptions only in exceptional situations. In that respect C++'s philosophy about using exceptions differs markedly from the way exceptions are used in, e.g., Java, where exceptions are often encountered in situations C++ doesn't consider exceptional. Another common practice is to follow a `conceptual' style when designing software. A nice characteristic of exceptions is that exceptions can be thrown at a point where your source shows what's happening: throwing an std::out_of_range exception is nice for the software maintainer, as the reason for the exception is immediately recognized.

At the catch-clause the semantical context usually isn't very relevant anymore and by catching a std::exception and showing its what() content the program's user is informed about what happened.

But throwing values of other types can also be useful. What about a situation where you want to throw an exception and catch it at some shallow level? In between there may be various levels of software provided by external software libraries over which the software engineer has no control. At those levels exceptions (std::exceptions) could be generated too, and those exceptions might also be caught by the library's code. When throwing a standard exception type it may be hard to convince yourself that that exception isn't caught by the externally provided software. Assuming that no catch-alls are used (i.e., catch (...)) then throwing an exception from the std::exception family might not be a very good idea. In such cases throwing a value from a simple, maybe empty, enum works fine:

    enum HorribleEvent 
    {};      

    ... at some deep level:
        throw HorribleEvent{};

    ... at some shallow level:
    catch (HorribleEvent hs)
    {
        ...
    }

Other examples can easily be found: design a class holding a message and an error (exit) code: where necessary throw an object of that class, catch it in the catch clause of main's try block and you can be sure that all objects defined at intermediate levels are neatly destroyed, and at the end you show the error message and return the exit code embedded in your non-exception object.

So, the advice is to use std::exception types when available, and clearly do the required job. But if an exception is used to simply bail out of an unpleasant situation, or if there's a chance that externally provided code might catch std:exceptions then consider throwing objects or values of other types.

10.9: System error, error_category, and error_condition

The class std::system_error is derived from std::runtime_error, which in turn is derived from std::exception

Before using the class system_error or related classes the <system_error> header file must be included.

System_error exceptions can be thrown when errors occur having associated (system) error values. Such errors are typically associated with low-level (like operating system) functions, but other types of errors (e.g., bad user input, non-existing requests) can also be handled.

In addition to error codes (cf. section 4.3.2) and error categories (covered below) error conditions are distinguished. Error conditions specify platform independent types of errors like syntax errors or non-existing requests.

When constructing system_error objects error codes and error categories may be specified. First we'll look at the classes error_condition and error_category, then system_error itself is covered in more detail.

Figure 9 illustrates how the various components interact.

Figure 9: System_error: associated components

As shown in figure 9 the class error_category uses the class error_condition and the class error_condition uses the class error_category. As a consequence of this circular dependency between these two classes these classes should be approached as one single class: when covering error_category the class error_condition should be known and vice versa. This circular dependency among these classes is unfortunate and an example of bad class design.

As system_error is eventually derived from exception it offers the standard what member. It also contains an error_code.

In POSIX systems the errno variable is associated with many, often rather cryptic, symbols. The predefined enum class errc attempts to provide intuitively more appealing symbols. Since its symbols are defined in a strongly typed enumeration, they cannot directly be used when defining a matching error_code. Instead, a make_error_code function converts enum class errc values and values of newly defined error code enumerations (called ErrorCodeEnum below) to error_code objects.

The enum class errc defined in the std namespace defines symbols whose values are equal to the traditional error code values used by C but describe the errors in a less cryptic way. E.g.,

    enum class errc 
    {
        address_family_not_supported, // EAFNOSUPPORT
        address_in_use,               // EADDRINUSE
        address_not_available,        // EADDRNOTAVAIL
        already_connected,            // EISCONN
        argument_list_too_long,       // E2BIG
        argument_out_of_domain,       // EDOM
        bad_address,                  // EFAULT
        ...
    };

Values of ErrorCodeEnums can be passed to matching make_error_code functions. Defining your own ErrorCodeEnum enumeration is covered in section 23.7.

Now that the general outline has been presented, it's time to have a closer look at the various components shown in figure 9.

10.9.1: The class `std::error_category'

Objects of the class std::error_category identify sources of sets of error codes. New error categories for new error code enumerations can also be defined (cf. section 23.7).

Error categories are designed as singletons: only one object of each class can exist. Because of this error_categories are equal when the addresses of error_category objects are equal. Error category objects are returned by functions (see below) or by static instance() members of error category classes.

Error category classes define several members. Most are declared virtual (cf. chapter 14), meaning that those members may be redefined in error category classes we ourselves design:

The functions returning predefined error categories are:

10.9.2: The class `std::error_condition'

Error_condition objects contain information about `higher level' types of errors. They are supposed to be platform independent like syntax errors or non-existing requests.

Error condition objects are returned by the member default_error_condition of the classes error_code and error_category, and they are returned by the function std::error_condition make_error_condition(ErrorConditionEnum ec). The type name ErrorConditionEnum is a formal name for an enum class that enumerates the `higher level' error types. The error_condition objects returned by make_error_condition are initialized with ec and the error_category that uses the ErrorConditionEnum. Defining your own ErrorConditionEnum is covered in section 23.7.

Constructors:

Members:

Two error_condition objects can be compared for (in)equality, and can be ordered using operator<. Ordering is pointless if the two objects refer to different error categories. If the categories of two objects are different they are considered different.

10.9.3: The class system_error

System_error objects can be constructed from error_codes or from error values (ints) and matching error category objects, optionally followed by a standard textual description of the nature of the encountered error.

Here is the class's public interface:

   class system_error: public runtime_error 
    {
        public:
            system_error(error_code ec);
            system_error(error_code ec, string const &what_arg);
            system_error(error_code ec, char const *what_arg);
    
            system_error(int ev, error_category const &ecat);
            system_error(int ev, error_category const &ecat,
                             char const *what_arg);
            system_error(int ev, error_category const &ecat,
                             string const &what_arg);
    
            error_code const &code() const noexcept;
            char const *what() const noexcept;
    }

The ev values often are the values of the errno variable as set upon failure by system level functions like chmod(2).

Note that the first three constructors shown in the interface receive an error_code object as their first arguments. As one of the error_code constructors also expects an int and and error_category argument, the second set of three constructors could also be used instead of the first set of three constructors. E.g.,

    system_error(errno, system_category(), "context of the error");
    // identical to:
    system_error(error_code(errno, system_category()), 
                                            "context of the error");

The second set of three constructors are primarily used when an existing function already returns an error_code. E.g.,

    system_error(make_error_code(errc::bad_address), 
                                            "context of the error");
    // or maybe:
    system_error(make_error_code(static_cast<errc>(errno)), 
                                            "context of the error");

In addition to the standard what member, the system_error class also offers a member code returning a const reference to the exception's error code.

The NTBS returned by system_error's what member may be formatted by a system_error object:

    what_arg + ": " + code().message()

Note that, although system_error was derived from runtime_error, you'll lose the code member when catching a std::exception object. Of course, downcasting is possible, but that's a stopgap. Therefore, if a system_error is thrown, a matching catch(system_error const &) clause must be provided to retrieve the value returned by the code member. This, and the rather complex organization of the classes that are involved when using system_error result in a very complex, and hard to generalize exception handling. In essence, what you obtain at the cost of high complexity is a facility for categorizing int or enum error values. Additional coverage of the involved complexities is provided in chapter 23, in particular section 23.7 (for a flexible alternative, see the class FBB::Exception in the author's Bobcat library).

10.10: Exception guarantees

Software should be exception safe: the program should continue to work according to its specifications in the face of exceptions. It is not always easy to realize exception safety. In this section some guidelines and terminology is introduced when discussing exception safety.

Since exceptions may be generated from within all C++ functions, exceptions may be generated in many situations. Not all of these situations are immediately and intuitively recognized as situations where exceptions can be thrown. Consider the following function and ask yourself at which points exceptions may be thrown:

    void fun()
    {
        X x;
        cout << x;
        X *xp = new X{ x };
        cout << (x + *xp);
        delete xp;
    }

If it can be assumed that cout as used above does not throw an exception there are at least 13 opportunities for exceptions to be thrown:

It is stressed here (and further discussed in section 10.12) that although it is possible for exceptions to leave destructors this would violate the C++ standard and so it must be prevented in well-behaving C++ programs.

How can we expect to create working programs when exceptions might be thrown in so many situations?

Exceptions may be generated in a great many situations, but serious problems are prevented when we're able to provide at least one of the following exception guarantees:

10.10.1: The basic guarantee

The basic guarantee dictates that functions that fail to complete their assigned tasks must return all allocated resources, usually memory, before terminating. Since practically all functions and operators may throw exceptions and since a function may repeatedly allocate resources the blueprint of a function allocating resources shown below defines a try block to catch all exceptions that might be thrown. The catch handler's task is to return all allocated resources and then rethrow the exception.
    void allocator(X **xDest, Y **yDest)
    {
        X *xp = 0;              // non-throwing preamble
        Y *yp = 0;

        try                     // this part might throw
        {
            xp = new X[nX];     // alternatively: allocate one object
            yp = new Y[nY];
        }
        catch(...)
        {
            delete xp;
            throw;
        }

        delete[] *xDest;        // non-throwing postamble
        *xDest = xp;
        delete[] *yDest;
        *yDest = yp;
    }

In the pre-try code the pointers to receive the addresses returned by the operator new calls are initialized to 0. Since the catch handler must be able to return allocated memory they must be available outside of the try block. If the allocation succeeds the memory pointed to by the destination pointers is returned and then the pointers are given new values.

Allocation and or initialization might fail. If allocation fails new throws a std::bad_alloc exception and the catch handler simply deletes 0-pointers which is OK.

If allocation succeeds but the construction of (some) of the objects fails by throwing an exception then the following is guaranteed to happen:

Consequently, there is no memory leak when new fails. Inside the above try block new X may fail: this does not affect the 0-pointers and so the catch handler merely deletes 0 pointers. When new Y fails xp points to allocated memory and so it must be returned. This happens inside the catch handler. The final pointer (here: yp) will only be unequal zero when new Y properly completes, so there's no need for the catch handler to return the memory pointed at by yp.

10.10.2: The strong guarantee

The strong guarantee dictates that an object's state should not change in the face of exceptions. This is realized by performing all operations that might throw on a separate copy of the data. If all this succeeds then the current object and its (now successfully modified) copy are swapped. An example of this approach can be observed in the canonical overloaded assignment operator:
    Class &operator=(Class const &other)
    {
        Class tmp(other);
        swap(tmp);
        return *this;
    }

The copy construction might throw an exception, but this keeps the current object's state intact. If the copy construction succeeds swap swaps the current object's content with tmp's content and returns a reference to the current object. For this to succeed it must be guaranteed that swap won't throw an exception. Returning a reference (or a value of a primitive data type) is also guaranteed not to throw exceptions. The canonical form of the overloaded assignment operator therefore meets the requirements of the strong guarantee.

Some rules of thumb were formulated that relate to the strong guarantee (cf. Sutter, H., Exceptional C++, Addison-Wesley, 2000). E.g.,

The canonical assignment operator is a good example of the first rule of thumb. Another example is found in classes storing objects. Consider a class PersonDb storing multiple Person objects. Such a class might offer a member void add(Person const &next). A plain implementation of this function (merely intended to show the application of the first rule of thumb, but otherwise completely disregarding efficiency considerations) might be:

    Person *PersonDb::newAppend(Person const &next)
    {
        Person *tmp = 0;
        try
        {
            tmp = new Person[d_size + 1];
            for (size_t idx = 0; idx < d_size; ++idx)
                tmp[idx] = d_data[idx];
            tmp[d_size] = next;
            return tmp;
        }
        catch (...)
        {
            delete[] tmp;
            throw;
        }
    }

    void PersonDb::add(Person const &next)
    {
        Person *tmp = newAppend(next);
        delete[] d_data;
        d_data = tmp;
        ++d_size;
    }

The (private) newAppend member's task is to create a copy of the currently allocated Person objects, including the data of the next Person object. Its catch handler catches any exception that might be thrown during the allocation or copy process and returns all memory allocated so far, rethrowing the exception at the end. The function is exception neutral as it propagates all its exceptions to its caller. The function also doesn't modify the PersonDb object's data, so it meets the strong exception guarantee. Returning from newAppend the member add may now modify its data. Its existing data are returned and its d_data pointer is made to point to the newly created array of Person objects. Finally its d_size is incremented. As these three steps don't throw exceptions add too meets the strong guarantee.

The second rule of thumb (member functions modifying their object's data should not return original (contained) objects by value) may be illustrated using a member PersonDb::erase(size_t idx). Here is an implementation attempting to return the original d_data[idx] object:

    Person PersonData::erase(size_t idx)
    {
        if (idx >= d_size)
            throw "Array bounds exceeded"s;
        Person ret(d_data[idx]);
        Person *tmp = copyAllBut(idx);
        delete[] d_data;
        d_data = tmp;
        --d_size;
        return ret;
    }

Although copy elision usually prevents the use of the copy constructor when returning ret, this is not guaranteed to happen. Furthermore, a copy constructor may throw an exception. If that happens the function has irrevocably mutated the PersonDb's data, thus losing the strong guarantee.

Rather than returning d_data[idx] by value it might be assigned to an external Person object before mutating PersonDb's data:

    void PersonData::erase(Person *dest, size_t idx)
    {
        if (idx >= d_size)
            throw "Array bounds exceeded"s;
        *dest = d_data[idx];
        Person *tmp = copyAllBut(idx);
        delete[] d_data;
        d_data = tmp;
        --d_size;
    }

This modification works, but changes the original assignment of creating a member returning the original object. However, both functions suffer from a task overload as they modify PersonDb's data and also return an original object. In situations like these the one-function-one-responsibility rule of thumb should be kept in mind: a function should have a single, well defined responsibility.

The preferred approach is to retrieve PersonDb's objects using a member like Person const &at(size_t idx) const and to erase an object using a member like void PersonData::erase(size_t idx).

10.10.3: The nothrow guarantee

Exception safety can only be realized if some functions and operations are guaranteed not to throw exceptions. This is called the nothrow guarantee. An example of a function that must offer the nothrow guarantee is the swap function. Consider once again the canonical overloaded assignment operator:
    Class &operator=(Class const &other)
    {
        Class tmp(other);
        swap(tmp);
        return *this;
    }

If swap were allowed to throw exceptions then it would most likely leave the current object in a partially swapped state. As a result the current object's state would most likely have been changed. As tmp has been destroyed by the time a catch handler receives the thrown exception it becomes very difficult (as in: impossible) to retrieve the object's original state. Losing the strong guarantee as a consequence.

The swap function must therefore offer the nothrow guarantee. It must have been designed as if using the following prototype (see also section 23.8):

    void Class::swap(Class &other) noexcept;

Likewise, operator delete and operator delete[] offer the nothrow guarantee, and according to the C++ standard destructors may themselves not throw exceptions (if they do their behavior is formally undefined, see also section 10.12 below).

Since the C programming language does not define the exception concept functions from the standard C library offer the nothrow guarantee by implication. This allowed us to define the generic swap function in section 9.6 using memcpy.

Operations on primitive types offer the nothrow guarantee. Pointers may be reassigned, references may be returned etc. etc. without having to worry about exceptions that might be thrown.

10.11: Function try blocks

Exceptions may be generated from inside constructors. How can exceptions generated in such situations be caught by the constructor itself, rather than outside the constructor? The intuitive solution, nesting the object construction in a try block does not solve the problem. The exception by then has left the constructor and the object we intended to construct isn't visible anymore.

Using a nested try block is illustrated in the next example, where main defines an object of class PersonDb. Assuming that PersonDb's constructor throws an exception, there is no way we can access the resources that might have been allocated by PersonDb's constructor from the catch handler as the pdb object is out of scope:

    int main(int argc, char **argv)
    {
        try
        {
            PersonDb pdb{ argc, argv }; // may throw exceptions
            ...                         // main()'s other code
        }
        catch(...)                      // and/or other handlers
        {
            ...                         // pdb is inaccessible from here
        }
    }

Although all objects and variables defined inside a try block are inaccessible from its associated catch handlers, object data members were available before starting the try block and so they may be accessed from a catch handler. In the following example the catch handler in PersonDb's constructor is able to access its d_data member:

    PersonDb::PersonDb(int argc, char **argv)
    :
        d_data(0),
        d_size(0)
    {
        try
        {
            initialize(argc, argv);
        }
        catch(...)
        {
            // d_data, d_size: accessible
        }
    }

Unfortunately, this does not help us much. The initialize member is unable to reassign d_data and d_size if PersonDb const pdb was defined; the initialize member should at least offer the basic exception guarantee and return any resources it has acquired before terminating due to a thrown exception; and although d_data and d_size offer the nothrow guarantee as they are of primitive data types a class type data member might throw an exception, possibly resulting in violation of the basic guarantee.

In the next implementation of PersonDb assume that constructor receives a pointer to an already allocated block of Person objects. The PersonDb object takes ownership of the allocated memory and it is therefore responsible for the allocated memory's eventual destruction. Moreover, d_data and d_size are also used by a composed object PersonDbSupport, having a constructor expecting a Person const * and size_t argument. Our next implementation may then look something like this:

    PersonDb::PersonDb(Person *pData, size_t size)
    :
        d_data(pData),
        d_size(size),
        d_support(d_data, d_size)
    {
        // no further actions
    }

This setup allows us to define a PersonDb const &pdb. Unfortunately, PersonDb cannot offer the basic guarantee. If PersonDbSupport's constructor throws an exception it isn't caught although d_data already points to allocated memory.

The function try block offers a solution for this problem. A function try block consists of a try block and its associated handlers. The function try block starts immediately after the function header, and its block defines the function body. With constructors base class and data member initializers may be placed between the try keyword and the opening curly brace. Here is our final implementation of PersonDb, now offering the basic guarantee:

    PersonDb::PersonDb(Person *pData, size_t size)
    try
    :
        d_data(pData),
        d_size(size),
        d_support(d_data, d_size)
    {}
    catch (...)
    {
        delete[] d_data;
    }

Let's have a look at a stripped-down example. A constructor defines a function try block. The exception thrown by the Throw object is initially caught by the object itself. Then it is rethrown. The surrounding Composer's constructor also defines a function try block, Throw's rethrown exception is properly caught by Composer's exception handler, even though the exception was generated from within its member initializer list:

    #include <iostream>

    class Throw
    {
        public:
            Throw(int value)
            try
            {
                throw value;
            }
            catch(...)
            {
                std::cout << "Throw's exception handled locally by Throw()\n";
                throw;
            }
    };

    class Composer
    {
        Throw d_t;
        public:
            Composer()
            try             // NOTE: try precedes initializer list
            :
                d_t(5)
            {}
            catch(...)
            {
                std::cout << "Composer() caught exception as well\n";
            }
    };

    int main()
    {
        Composer c;
    }

When running this example, we're in for a nasty surprise: the program runs and then breaks with an abort exception. Here is the output it produces, the last two lines being added by the system's final catch-all handler, catching all remaining uncaught exceptions:

    Throw's exception handled locally by Throw()
    Composer() caught exception as well
    terminate called after throwing an instance of 'int'
    Abort

The reason for this is documented in the C++ standard: at the end of a catch-handler belonging to a constructor or destructor function try block, the original exception is automatically rethrown.

The exception is not rethrown if the handler itself throws another exception, offering the constructor or destructor a way to replace a thrown exception by another one. The exception is only rethrown if it reaches the end of the catch handler of a constructor or destructor function try block. Exceptions caught by nested catch handlers are not automatically rethrown.

As only constructors and destructors rethrow exceptions caught by their function try block catch handlers the run-time error encountered in the above example may simply be repaired by providing main with its own function try block:

    int main()
    try
    {
        Composer c;
    }
    catch (...)
    {}

Now the program runs as planned, producing the following output:

    Throw's exception handled locally by Throw()
    Composer() caught exception as well

A final note: if a function defining a function try block also declares an exception throw list then only the types of rethrown exceptions must match the types mentioned in the throw list.

10.12: Exceptions in constructors and destructors

Object destructors are only activated for completely constructed objects. Although this may sound like a truism, there is a subtlety here. If the construction of an object fails for some reason, the object's destructor is not called when the object goes out of scope. This could happen if an exception that is generated by the constructor is not caught by the constructor. If the exception is thrown when the object has already allocated some memory, then that memory is not returned: its destructor isn't called as the object's construction wasn't successfully completed.

The following example illustrates this situation in its prototypical form. The constructor of the class Incomplete first displays a message and then throws an exception. Its destructor also displays a message:

    class Incomplete
    {
        public:
            Incomplete()
            {
                cerr << "Allocated some memory\n";
                throw 0;
            }
            ~Incomplete()
            {
                cerr << "Destroying the allocated memory\n";
            }
    };
Next, main() creates an Incomplete object inside a try block. Any exception that may be generated is subsequently caught:
    int main()
    {
        try
        {
            cerr << "Creating `Incomplete' object\n";
            Incomplete{};
            cerr << "Object constructed\n";
        }
        catch(...)
        {
            cerr << "Caught exception\n";
        }
    }
When this program is run, it produces the following output:
    Creating `Incomplete' object
    Allocated some memory
    Caught exception

Thus, if Incomplete's constructor would actually have allocated some memory, the program would suffer from a memory leak. To prevent this from happening, the following counter measures are available:

The catch clause of a constructor's function try block behaves slightly different than a catch clause of an ordinary function try block. An exception reaching a constructor's function try block may be transformed into another exception (which is thrown from the catch clause) but if no exception is explicitly thrown from the catch clause the exception originally reaching the catch clause is always rethrown. Consequently, there's no way to confine an exception thrown from a base class constructor or from a member initializer to the constructor: such an exception always propagates to a more shallow block and in that case the object's construction is always considered incomplete.

Consequently, if incompletely constructed objects throw exceptions then the constructor's catch clause is responsible for preventing memory (generally: resource) leaks. There are several ways to realize this:

On the other hand, since C++ supports constructor delegation an object may have been completely constructed according to the C++ run-time system, but yet its constructor may have thrown an exception. This happens if a delegated constructor successfully completes (after which the object is considered `completely constructed'), but the constructor itself throws an exception, as illustrated by the next example:

    class Delegate
    {
        public:
            Delegate()
            :
                Delegate(0)
            {
                throw 12;       // throws but completely constructed
            }
            Delegate(int x)         // completes OK
            {}
    };
    int main()
    try
    {
        Delegate del;           // throws

    } // del's destructor is called here
    catch (...)
    {}

In this example it is the responsibility of Delegate's designer to ensure that the throwing default constructor does not invalidate the actions performed by Delegate's destructor. E.g., if the delegated constructor allocates memory to be deleted by the destructor, then the default constructor should either leave the memory as-is, or it can delete the memory and set the corresponding pointer to zero thereafter. In any case, it is Delegate's responsibility to ensure that the object remains in a valid state, even though it throws an exception.

According to the C++ standard exceptions thrown by destructors may not leave their bodies. Providing a destructor with a function try block is therefore a violation of the standard: exceptions caught by a function try block's catch clause have already left the destructor's body. If --in violation of the standard-- the destructor is provided with a function try block and an exception is caught by the try block then that exception is rethrown, similar to what happens in catch clauses of constructor functions' try blocks.

The consequences of an exception leaving the destructor's body is not defined, and may result in unexpected behavior. Consider the following example:

Assume a carpenter builds a cupboard having a single drawer. The cupboard is finished, and a customer, buying the cupboard, finds that the cupboard can be used as expected. Satisfied with the cupboard, the customer asks the carpenter to build another cupboard, this time having two drawers. When the second cupboard is finished, the customer takes it home and is utterly amazed when the second cupboard completely collapses immediately after it is used for the first time.

Weird story? Then consider the following program:

    int main()
    {
        try
        {
            cerr << "Creating Cupboard1\n";
            Cupboard1{};
            cerr << "Beyond Cupboard1 object\n";
        }
        catch (...)
        {
            cerr << "Cupboard1 behaves as expected\n";
        }
        try
        {
            cerr << "Creating Cupboard2\n";
            Cupboard2{};
            cerr << "Beyond Cupboard2 object\n";
        }
        catch (...)
        {
            cerr << "Cupboard2 behaves as expected\n";
        }
    }
When this program is run it produces the following output:
    Creating Cupboard1
    Drawer 1 used
    Cupboard1 behaves as expected
    Creating Cupboard2
    Drawer 2 used
    Drawer 1 used
    terminate called after throwing an instance of 'int'
    Abort

The final Abort indicates that the program has aborted instead of displaying a message like Cupboard2 behaves as expected.

Let's have a look at the three classes involved. The class Drawer has no particular characteristics, except that its destructor throws an exception:

    class Drawer
    {
        size_t d_nr;
        public:
            Drawer(size_t nr)
            :
                d_nr(nr)
            {}
            ~Drawer()
            {
                cerr << "Drawer " << d_nr << " used\n";
                throw 0;
            }
    };
The class Cupboard1 has no special characteristics at all. It merely has a single composed Drawer object:
    class Cupboard1
    {
        Drawer left;
        public:
            Cupboard1()
            :
                left(1)
            {}
    };
The class Cupboard2 is constructed comparably, but it has two composed Drawer objects:
    class Cupboard2
    {
        Drawer left;
        Drawer right;
        public:
            Cupboard2()
            :
                left(1),
                right(2)
            {}
    };

When Cupboard1's destructor is called Drawer's destructor is eventually called to destroy its composed object. This destructor throws an exception, which is caught beyond the program's first try block. This behavior is completely as expected.

A subtlety here is that Cupboard1's destructor (and hence Drawer's destructor) is activated immediately subsequent to its construction. Its destructor is called immediately subsequent to its construction as Cupboard1() defines an anonymous object. As a result the Beyond Cupboard1 object text is never inserted into std::cerr.

Because of Drawer's destructor throwing an exception a problem occurs when Cupboard2's destructor is called. Of its two composed objects, the second Drawer's destructor is called first. This destructor throws an exception, which ought to be caught beyond the program's second try block. However, although the flow of control by then has left the context of Cupboard2's destructor, that object hasn't completely been destroyed yet as the destructor of its other (left) Drawer still has to be called.

Normally that would not be a big problem: once an exception is thrown from Cupboard2's destructor any remaining actions would simply be ignored, albeit that (as both drawers are properly constructed objects) left's destructor would still have to be called.

This happens here too and left's destructor also needs to throw an exception. But as we've already left the context of the second try block, the current flow control is now thoroughly mixed up, and the program has no other option but to abort. It does so by calling terminate(), which in turn calls abort(). Here we have our collapsing cupboard having two drawers, even though the cupboard having one drawer behaves perfectly.

The program aborts since there are multiple composed objects whose destructors throw exceptions leaving the destructors. In this situation one of the composed objects would throw an exception by the time the program's flow control has already left its proper context causing the program to abort.

The C++ standard therefore understandably stipulates that exceptions may never leave destructors. Here is the skeleton of a destructor whose code might throw exceptions. No function try block but all the destructor's actions are encapsulated in a try block nested under the destructor's body.

    Class::~Class()
    {
        try
        {
            maybe_throw_exceptions();
        }
        catch (...)
        {}
    }