Movatterモバイル変換


[0]ホーム

URL:




Chapter 10: Exceptions

C supports several ways for a program to react to situations breakingthe normal unhampered flow of a program: InC++ all theseflow-breaking methods are stillavailable. However, of the mentioned alternatives,setjmp andlongjmp isn't frequently encountered inC++ (or even inC)programs, due to the fact that the program flow is completely disrupted.

C++ offersexceptions as the preferred alternative to, e.g.,setjmp andlongjmp. Exceptions allowC++ programs to perform acontrollednon-local return, without the disadvantages oflongjmp andsetjmp.

Exceptions are the proper way to bail out of a situation which cannot behandled easily by a function itself, but which is not disastrous enough fora program to terminate completely. Also, exceptions provide a flexible layerof control between the short-rangereturn and the crudeexit.

In this chapter exceptions are covered. First an example is given of thedifferent impact exceptions and thesetjmp/longjmp combination have onprograms. This example is followed by a discussion of the formal aspectsof exceptions. In this part the guarantees our software should be ableto offer when confronted with exceptions are presented. Exceptions and theirguarantees have consequences for constructors and destructors. We'll encounterthese consequences at the end of this chapter.

10.1: Exception syntax

Before contrasting the traditionalC way of handling non-local gotos withexceptions let's introduce thesyntactic elements that are involved whenusing exceptions.

10.2: An example using exceptions

In the following examples the same basic program is used. The program usestwo classes,Outer andInner.

First, anOuter object is defined inmain, and its memberOuter::fun is called. Then, inOuter::fun anInner object isdefined. Having defined theInner object, its memberInner::fun iscalled.

That's about it. The functionOuter::fun terminates callinginner's destructor. Then the program terminates, activatingouter'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 callingsequence of the constructors).

Now let's focus our attention on two variants in which we simulate a non-fataldisastrous event in theInner::fun function. This event must supposedly behandled nearmain's end.

We'll consider two variants. In the first variant the event is handled bysetjmp andlongjmp; in the second variant the event is handled usingC++'s exception mechanism.

10.2.1: Anachronisms: `setjmp' and `longjmp'

The basic program from the previous section is slightly modified to contain avariablejmp_buf jmpBuf used bysetjmp andlongjmp.

The functionInner::fun callslongjmp, simulating a disastrousevent, to be handled nearmain's end. Inmain a target location forthe long jump is defined through the functionsetjmp.Setjmp's zeroreturn indicates the initialization of thejmp_buf variable, in which caseOuter::fun is called. This situation represents the `normal flow'.

The program's return value is zeroonly ifOuter::fun terminatesnormally. The program, however, is designed in such a way that this won'thappen:Inner::fun callslongjmp. As a result the execution flowreturns to thesetjmp function. In this case it doesnot return a zeroreturn value. Consequently, after callingInner::fun fromOuter::funmain'sif-statement is entered and the program terminates with returnvalue 1. Try to follow these steps when studying the following programsource, which is a direct modification of the basic program given in section10.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 thatinner's destructor is notcalled. This is a direct consequence of the non-local jump performed bylongjmp. Processing proceeds immediately from thelongjmp call insideInner::fun tosetjmp inmain. There, its return value is unequalzero, and the program terminates with return value 1. Because of the non-localjumpInner::~Inner is never executed: upon return tomain'ssetjmpthe existing stack is simply broken down disregarding any destructors waitingto be called.

This example illustrates that the destructors of objects can easily be skippedwhenlongjmp andsetjmp are used andC++ programs should thereforeavoid those functions like the plague.

10.2.2: Exceptions: the preferred alternative

Exceptions areC++'s answer to the problems caused bysetjmp andlongjmp. Here is an example using exceptions. The program is once againderived from the basic program of section10.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 anint exception where alongjmp waspreviously used. Sincein.fun is called byout.fun, the exception isgenerated within thetry block surrounding theout.fun call. As anint value was thrown this value reappears in thecatch clause beyondthetry block.

NowInner::fun terminates by throwing an exception instead of callinglongjmp. The exception is caught inmain, and the programterminates. Now we see thatinner's destructor is properly called. It isinteresting to note thatInner::fun's execution really terminates at thethrow statement: Thecout statement, placed just beyond thethrowstatement, isn't executed.

What did this example teach us?

10.3: Throwing exceptions

Exceptions are generated bythrow statements. Thethrow keyword isfollowed 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 nodifferent for exceptions.

Objects defined locally in functions are automatically destroyed onceexceptions thrown by these functions leave these functions. This also happensto objects thrown as exceptions. However, just before leaving the functioncontext the object is copied and it is this copy that eventually reaches theappropriatecatch clause.

The following examples illustrates this process.Object::fun defines a localObject toThrow, that isthrown as an exception. The exception is caughtinmain. But by then the object originally thrown doesn't exist anymore,andmain 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 asthe other object's name to which the string" (copy)" is appended. Thisallow us to monitor the construction and destruction of objects more closely.Object::fun generates an exception, and throws its locally definedobject. Just before throwing the exception the program has produced thefollowing 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 tothrow where it is treated as a valueargument, creating a copy oftoThrow. This copy is thrown as theexception, and the localtoThrow object ceases to exist. The thrownexception is now caught by thecatch clause, defining anObject value parameter. Since this is avalue parameter yet anothercopy is created. Thus, the program writes the following text:

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

Thecatch block now displays:

    Caught exception

Following thiso'shello member is called, showing us that weindeed received acopy of the copy of the originaltoThrow object:

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

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

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

The copy created by thecatch clause clearly is superfluous. It can beavoided by defining objectreference parameters incatch 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 oftoThrow was created.

It's a bad idea to throw apointer to a locally definedobject. The pointer is thrown, but the object to which the pointer refersceases to exist once the exception is thrown. The catcher receives awild pointer. Bad news....

Let's summarize the above findings:

Exceptions are thrown in situations where a function can't complete itsassigned task, but the program is still able to continue. Imagine a programoffering an interactive calculator. The program expects numeric expressions,which are evaluated. Expressions may show syntactic errors or it may bemathematically impossible to evaluate them. Maybe the calculator allows us todefine and use variables and the user might refer to non-existing variables:plenty of reasons for the expression evaluation to fail, and so many reasonsfor exceptions to be thrown. None of those should terminate theprogram. Instead, the program's user is informed about the nature of theproblem 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 thesethrow statements are located is irrelevant: they may befound deeply nested inside the program, or at a more superficial level.Furthermore,functions may be used to generate the exception to bethrown. AnException object might support stream-like insertion operationsallowing 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 catchermay decide to ignore the exception, to process the exception, to rethrow itafter 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 byentering them into a queue. Normally every request is eventually answered bythe 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 mayhave died, and the client should be able to discover this calamity, by notwaiting indefinitely for the server to reply.

In this situation an intermediate exception handler is called for. A thrownexception is first inspected at the middle level. If possible it is processedthere. If it is not possible to process the exception at the middle level, itis passed on, unaltered, to a more superficial level, where the really toughexceptions are handled.

By placing anemptythrow statement in the exceptionhandler's code the received exception is passed on to the next level thatmight be able to process that particular type of exception. Therethrownexception is never handled by one of its neighboring exception handlers; itis 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 thestring exception. The received messageis inspected. If it's a simple message it's processed, otherwise the exceptionis passed on to an outer level. IninitialExceptionHandler'simplementation the emptythrow statement is used:

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

Below (section10.5), the emptythrow statement is usedto pass on the exception received by acatch-block. Therefore, a functionlikeinitialExceptionHandler can be used for a variety of thrownexceptions, as long as their types matchinitialExceptionHandler'sparameter, which is a string.

The next example jumps slightly ahead, using some of the topics covered inchapter14. The example may be skipped, though, withoutloss of continuity.

A basic exception handling class can be constructed from which specificexception types are derived. Suppose we have a classException, having amember functionExceptionType Exception::severity. This member functiontells us (little wonder!) the severity of a thrown exception. It might beInfo, Notice, Warning, Error orFatal. The information contained inthe exception depends on its severity and is processed by a functionhandle. In addition, all exceptions support a member function liketextMsg, returning textual information about the exception in astring.

By defining a polymorphic functionhandle it can be made to behavedifferently, depending on the nature of a thrown exception, when calledfrom a basicException pointer or reference.

In this case, a program may throw any of these five exception types. Assumingthat the classesMessage andWarning were derived from the classException, then thehandle function matching the exception type willautomatically 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 thetry block preceding the exception handlerException objects or objects of one of its derived classes may bethrown. 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

Thetry-block surroundsthrow statements. Remember that a program isalways surrounded by a globaltry block, sothrow statements mayappear anywhere in your code. More often, though,throw statements areused in function bodies and such functions may be called from withintryblocks.

Atry block is defined by the keywordtry followed by a compoundstatement. This block, in turn,must be followed by at least onecatch handler:

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

Try-blocks are commonly nested, creating exceptionlevels. Forexample,main's code is surrounded by atry-block, forming an outerlevel handling exceptions. Withinmain'stry-block functions arecalled which may also containtry-blocks, forming the next exceptionlevel. As we have seen (section10.3.1), exceptions thrown ininner leveltry-blocks may or may not be processed at that level. Byplacing an emptythrow statement in an exception handler, thethrown exception is passed on to the next (outer) level.

10.5: Catching exceptions

Acatch clause consists of the keywordcatch followed by a parameterlist defining one parameter specifying type and (parameter) name of theexception caught by that particularcatch handler. This name may then beused as a variable in the compound statement following thecatch clause.Example:
    catch (string &message)    {        // code to handle the message    }

Primitive types and objects may be thrown as exceptions. It's a bad ideato throw a pointer or reference to a local object, but a pointer to adynamically allocated object may be thrown if the exception handlerdeletes the allocated memory to prevent amemory leak. Nevertheless,throwing such a pointer is dangerous as the exception handler won't be able todistinguish dynamically allocated memory from non-dynamically allocatedmemory, 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 theexception handler, to make sure that when pointers to dynamically allocatedmemory are thrown the memory is returned once the handler has processedthe pointer. In general pointers should not be thrown as exceptions. Ifdynamically allocated memory must be passed to an exception handler then thepointer should be wrapped in a smart pointer, likeunique_ptr orshared_ptr (cf. sections18.3 and18.4).

Multiplecatch handlers may follow atry block, each handlerdefining its own exception type. Theorderof the exception handlers is important. When an exception is thrown, the firstexception handler matching the type of the thrown exception is used andremaining exception handlers are ignored. Eventually at most one exceptionhandler following atry-block is activated. Normally this is of noconcern as each exception has its own unique type.

Example: if exception handlers are defined forchar *s andvoid *sthen NTBSs are caught by the former handler. Note that achar* can also be considered avoid *, but the exception type matchingprocedure is smart enough to use thechar * handler with the thrownNTBS. Handlers should be designed very type specific to catch thecorrespondingly typed exception. For example,int-exceptions are notcaught bydouble-catchers,char-exceptions are not caught byint-catchers. Here is a little example illustrating that the order of thecatchers is not important for types not having any hierarchal relationship toeach other (i.e.,int is not derived fromdouble;string is notderived 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 bedesigned whose objects contain information about the exception. Such anapproach was mentioned earlier, in section10.3.1. Using thisapproach, there's only one handler required, since weknow we don't throwother 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 continuesbeyond the last exception handler directly following the matchingtry-block (assuming the handler doesn't itself use flow control statements(likereturn orthrow) to break the default flow of execution). Thefollowing cases can be distinguished:

All statements in atry block following an executedthrow-statement are ignored. However, objects that were successfullyconstructed within thetry block before executing thethrow statementare 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 actuallybe required. Exceptions whose types belong to that limited set are processed,all other exceptions are passed on to exception handlers of an outer leveltry block.

An intermediate type of exception handling may be implemented using thedefault exception handler, which must be (due to the hierarchal nature ofexception catchers, discussed in section10.5) placed beyondall other, more specific exception handlers.

This default exception handler cannot determine the actual type of the thrownexception and cannot determine the exception's value but it may execute somestatements, and thus do some default processing. Moreover, the caughtexception is not lost, and the default exception handler may use the emptythrow statement (see section10.3.1) to pass the exception on toan outer level, where it's actually processed. Here is an example showingthis 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 emptythrow statement in adefault 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 innerlevel, while specific handling, based on the type of the thrown expression,can be provided at an outer level. Additionally, particularly inmulti-threaded programs (cf. chapter20), thrown exceptions can betransferred between threads after convertingstd::exception objects tostd::exception_ptr objects. This proceduce can even be used from insidethe default catcher. Refer to section10.9.4 for further coverage of theclassstd::exception_ptr.

10.6: Functions unable to throw exceptions: the `noexcept' keyword

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

Functions that may not throw exceptions can be declared and defined byspecifying thenoexcept keyword (see section10.9 for examplesof function declarations specifyingnoexcept).

When usingnoexept there's a slight run-time overhead penalty because thefunction needs an over-alltry-catch block catching any exception thatmight be thrown by its (called) code. When an exception is caught (violatingthenoexcept specification) then thecatch clause callsstd::terminate, ending the program.

In addition to using a plainnoexcept, it can also be given an argumentthat is evaluated compile-time (e.g.,void fun() noexcept(sizeof(int) ==4)): if the evaluation returnstrue then thenoexcept requirement isused; if the evaluation returnsfalse, then thenoexcept requirementis ignored. Examples of this advanced use ofnoexcept are provided insection23.8.

10.7: Iostreams and exceptions

TheC++ I/O library was used well before exceptions were available inC++. Hence, normally the classes of the iostream library do not throwexceptions. However, it is possible to modify that behavior using theios::exceptions member function. This function has two overloadedversions:

In the I/O library, exceptions are objects of the classios::failure, derived fromios::exception. Astd::string const &message may be specified whendefining afailure object. Its message may then be retrieved using itsvirtual char const *what() const member.

Exceptions should be used in exceptional circumstances. Therefore, wethink it is questionable to have stream objects throw exceptions for fairlynormal situations likeEOF. Using exceptions to handle input errorsmight be defensible (e.g., in situations where input errors should not occurand imply a corrupted file) but often aborting the program with an appropriateerror message would probably be the more appropriate action. As an exampleconsider the following interactive program using exceptions to catch incorrectinput:

    #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 withinostream objects are caught bythese objects, which set theirios::badbit as a result. See also theparagraph on this issue in section14.8.

10.8: Standard exceptions

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

All of thesestandard exceptions are class types by themselves, but also offerall facilities of thestd::exception class and objectsof the standard exception classes may also be considered objects of thestd::exception class.

Thestd::exception class offers the member

    char const *what() const;

describing in a short textual message the nature of theexception.

C++ defines the following standard exception classes:

All additional exception classes were derived fromstd::exception. Theconstructors of all these additional classes acceptstd::string const &arguments summarizing the reason for the exception (retrieved by theexception::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 tothrow values of standard exception types and (if ever) when to throw values ofother types.

Current practice in the C++ community is to throw exceptions only inexceptional situations. In that respect C++'s philosophy about usingexceptions differs markedly from the way exceptions are used in, e.g., Java,where exceptions are often encountered in situations C++ doesn't considerexceptional. Another common practice is to follow a `conceptual' style whendesigning software. A nice characteristic of exceptions is that exceptions canbe thrown at a point where your source shows what's happening: throwing anstd::out_of_range exception is nice for the software maintainer, asthe reason for the exception is immediately recognized.

At the catch-clause the semantical context usually isn't very relevant anymoreand by catching a std::exception and showing itswhat() content theprogram'suser is informed about what happened.

But throwing values of other types can also be useful. What about a situationwhere you want to throw an exception and catch it at some shallow level? Inbetween there may be various levels of software provided by external softwarelibraries over which the software engineer has no control. At those levelsexceptions (std::exceptions) could be generated too, and those exceptionsmight also be caught by the library's code. When throwing a standard exceptiontype it may be hard to convince yourself that that exception isn't caught bythe externally provided software. Assuming that no catch-alls are used (i.e.,catch (...)) then throwing an exception from thestd::exceptionfamily might not be a very good idea. In such cases throwing a value from asimple, 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 andan error (exit) code: where necessary throw an object of that class, catch itin the catch clause of main's try block and you can be sure that all objectsdefined at intermediate levels are neatly destroyed, and at the end you showthe error message and return the exit code embedded in your non-exceptionobject.

So, the advice is to usestd::exception types when available, andclearly do the required job. But if an exception is used to simply bail outof an unpleasant situation, or if there's a chance that externally providedcode might catchstd:exceptions then consider throwing objects or valuesof other types.

10.9: System error, error_category, and error_condition

The classstd::system_error is derived fromstd::runtime_error, which in turn is derived fromstd::exception

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

System_error exceptions can be thrown when errors occur havingassociated (system)error values. Such errors are typically associatedwith 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. section4.3.1) and error categories(covered below) errorconditions are distinguished. Errorconditions specify platform independent types of errors like syntax errors ornon-existing requests.

When constructingsystem_error objects error codes and error categoriesmay be specified. First we'll look at the classeserror_condition anderror_category, thensystem_error itself is covered in more detail.

Figure9 illustrates how the various components interact.

Figure 9: System_error: associated components

As shown in figure9 the classerror_category uses the classerror_condition and the classerror_condition uses the classerror_category. As a consequence of this circular dependency between thesetwo classes these classes should be approached as one single class:when coveringerror_category the classerror_condition should be knownand vice versa. This circular dependency among these classes is unfortunateand an example of bad class design.

Assystem_error is eventually derived fromexception it offers thestandardwhat member. It also contains anerror_code.

In POSIX systems theerrno variable is associated with many, often rathercryptic, symbols. The predefinedenum class errc attempts to provideintuitively more appealing symbols. Since its symbols are defined in astrongly typed enumeration, they cannot directly be used when defining amatchingerror_code. Instead, amake_error_code function convertsenum class errc values and values of newly definederror code enumerations (calledErrorCodeEnum below) toerror_codeobjects.

Theenum class errc defined in thestd namespace defines symbols whosevalues are equal to the traditional error code values used byC 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 ofErrorCodeEnums can be passed to matchingmake_error_codefunctions. Defining your ownErrorCodeEnum enumeration is covered insection23.7.

Now that the general outline has been presented, it's time to have a closerlook at the various components shown in figure9.

10.9.1: The class `std::error_category'

Objects of the classstd::error_category identify sources of sets of errorcodes. New error categories for new error code enumerations can also bedefined (cf. section23.7).

Error categories are designed assingletons: only one object of each classcan exist. Because of thiserror_categories are equal when the addressesoferror_category objects are equal. Error category objects are returnedby functions (see below) or by staticinstance() members of error categoryclasses.

Error category classes define several members. Most are declaredvirtual(cf. chapter14), meaning that those members may be redefinedin 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 independentlike syntax errors or non-existing requests.

Error condition objects are returned by the memberdefault_error_conditionof the classeserror_code anderror_category, and they are returned bythe functionstd::error_conditionmake_error_condition(ErrorConditionEnum ec). The type nameErrorConditionEnum is a formal name for anenum class that enumeratesthe `higher level' error types. Theerror_condition objects returned bymake_error_condition are initialized withec and theerror_category that uses theErrorConditionEnum. Defining your ownErrorConditionEnum is covered in section23.7.

Constructors:

Members:

Twoerror_condition objects can be compared for (in)equality, and can beordered usingoperator<. Ordering is pointless if the two objects refer todifferent 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 fromerror_codes or from error values (ints) and matching error category objects, optionallyfollowed by a standard textual description of the nature of the encounterederror.

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;    }

Theev values often are the values of theerrno variable as setupon failure by system level functions likechmod(2).

Note that the first three constructors shown in the interface receive anerror_code object as their first arguments. As one of theerror_codeconstructors also expects anint and anderror_category argument,the second set of three constructors could also be used instead of the firstset 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 existingfunction already returns anerror_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 standardwhat member, thesystem_error class alsooffers a membercode returning a const reference to the exception's errorcode.

The NTBS returned bysystem_error's what member may be formatted by asystem_error object:

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

Note that, althoughsystem_error was derived fromruntime_error,you'll lose thecode member when catching astd::exception object. Ofcourse, downcasting is possible, but that's a stopgap. Therefore, if asystem_error is thrown, a matchingcatch(system_error const &) clausemust be provided to retrieve the value returned by thecode member. This,and the rather complex organization of the classes that are involved whenusingsystem_error result in a very complex, and hard to generalizeexception handling. In essence, what you obtain at the cost of highcomplexity is a facility for categorizingint orenum errorvalues. Additional coverage of the involved complexities is provided inchapter23, in particular section23.7(for a flexible alternative, see the classFBB::Exception in the author'sBobcat library).

10.9.4: Exception propagation: std::exception_ptr

In practice almost everything can be used as an exception. But at the sametime any exception that is thrown can be reached using the function(std::current_exception), and access to any exceptioncan be standardized usingstd::make_exception_ptr.These functions expect or use objects of the classstd::exception_ptr, and in this section we take a closerlook at tha class.

The classexception_ptr's default constructor initializes it to anull-pointer. In the following code snippet the variableisNull is set totrue:

    std::exception_ptr obj;    bool isNull =  obj == nullptr && obj == 0;

The classexception_ptr provides copy and move constructors as well ascopy and move assignment operators.

Twoexception_ptr objects can be compared for equality. They are equalif they refer to the same exception. Move assignment transfers the exceptionreferred to by the right-hand side operand to the left-hand side operand, andturns the right-hand side operand into a null pointer.

There is no published method directly retrieving the exception to which anexception_ptr object refers. However, there are some free functionsconstructing or handlingexception_ptr objects:

10.10: Exception guarantees

Software should beexception safe: the program should continue to workaccording to its specifications in the face of exceptions. It is not alwayseasy to realize exception safety. In this section some guidelines andterminology is introduced when discussing exception safety.

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

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

If it can be assumed thatcout as used above does not throw anexception there are at least 13 opportunities for exceptions to be thrown:

It is stressed here (and further discussed in section10.12)that although it is possible for exceptions to leavedestructors this would violate theC++ standard and so it must beprevented in well-behavingC++ programs.

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

Exceptions may be generated in a great many situations, but seriousproblems are prevented when we're able to provide at least one of thefollowingexception guarantees:

10.10.1: The basic guarantee

Thebasic guarantee dictates that functions that fail to complete theirassigned tasks must return all allocated resources, usually memory, beforeterminating. Since practically all functions and operators may throwexceptions and since a function may repeatedly allocate resources theblueprint of a function allocating resources shown below defines a try blockto catch all exceptions that might be thrown. The catch handler's task is toreturn 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 theoperatornew calls are initialized to 0. Since the catch handler must beable to return allocated memory they must be available outside of thetryblock. If the allocation succeeds the memory pointed to by the destinationpointers is returned and then the pointers are given new values.

Allocation and or initialization might fail. If allocation failsnewthrows astd::bad_alloc exception and the catch handlersimply deletes 0-pointers which is OK.

If allocation succeeds but the construction of (some) of the objects failsby throwing an exception then the following isguaranteed to happen:

Consequently, there is no memory leak whennew fails. Inside the abovetry blocknew X may fail: this does not affect the 0-pointersand so the catch handler merely deletes 0 pointers. Whennew Y failsxp points to allocated memory and so it must be returned. This happensinside the catch handler. The final pointer (here:yp) will only beunequal zero whennew Y properly completes, so there's no need for thecatch handler to return the memory pointed at byyp.

10.10.2: The strong guarantee

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

The copy construction might throw an exception, but this keeps the currentobject's state intact. If the copy construction succeedsswap swaps thecurrent object's content withtmp's content and returns a reference tothe current object. For this to succeed it must be guaranteed thatswapwon't throw an exception. Returning a reference (or a value of a primitivedata type) is also guaranteed not to throw exceptions. The canonical form ofthe overloaded assignment operator therefore meets the requirements of thestrong guarantee.

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

The canonical assignment operator is a good example of the first rule ofthumb. Another example is found in classes storing objects. Consider a classPersonDb storing multiplePerson objects. Such a class might offer amembervoid add(Person const &next). A plain implementation of thisfunction (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 thecurrently allocatedPerson objects, including the data of the nextPerson object. Itscatch handler catches any exception that might bethrown during the allocation or copy process and returns all memoryallocated so far, rethrowing the exception at the end. The function isexception neutral as it propagates all its exceptions to its caller. Thefunction also doesn't modify thePersonDb object's data, so it meets thestrong exception guarantee. Returning fromnewAppend the memberaddmay now modify its data. Its existing data are returned and itsd_datapointer is made to point to the newly created array ofPersonobjects. Finally itsd_size is incremented. As these three steps don'tthrow exceptionsadd too meets the strong guarantee.

The second rule of thumb (member functions modifying their object's datashould not return original (contained) objects by value) may be illustratedusing a memberPersonDb::erase(size_t idx). Here is an implementationattempting to return the originald_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 constructorwhen returningret, this is not guaranteed to happen. Furthermore, a copyconstructormay throw an exception. If that happens the function hasirrevocably mutated thePersonDb's data, thus losing the strong guarantee.

Rather than returningd_data[idx] by value it might be assigned to anexternalPerson object before mutatingPersonDb'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 amember returning the original object. However, both functions suffer from atask overload as they modifyPersonDb's data and also return an originalobject. In situations like these theone-function-one-responsibilityrule of thumb should be kept in mind: a function should have a single, welldefined responsibility.

The preferred approach is to retrievePersonDb's objects using a memberlikePerson const &at(size_t idx) const and to erase an object using amember likevoid PersonData::erase(size_t idx).

10.10.3: The nothrow guarantee

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

Ifswap were allowed to throw exceptions then it would most likelyleave the current object in a partially swapped state. As a result the currentobject's state would most likely have been changed. Astmp has beendestroyed by the time a catch handler receives the thrown exception it becomesvery difficult (as in: impossible) to retrieve the object's originalstate. Losing the strong guarantee as a consequence.

Theswap function must therefore offer the nothrow guarantee. It musthave been designed as if using the following prototype (see also section23.8):

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

Likewise,operator delete andoperator delete[] offer the nothrowguarantee, and according to theC++ standard destructors may themselvesnot throw exceptions (if they do their behavior is formally undefined, seealso section10.12 below).

Since theC programming language does not define the exception conceptfunctions from the standardC library offer the nothrow guaranteeby implication. This allowed us to define the genericswap function insection9.6 usingmemcpy.

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

10.11: Function try blocks

Exceptions may be generated from inside constructors. How can exceptionsgenerated in such situations be caught by the constructor itself, rather thanoutside the constructor? The intuitive solution, nesting the objectconstruction in atry block does not solve the problem. The exception bythen has left the constructor and the object we intended to construct isn'tvisible anymore.

Using a nestedtry block is illustrated in the next example, wheremain defines an object of classPersonDb. Assuming thatPersonDb's constructor throws an exception, there is no way we can accessthe resources that might have been allocated byPersonDb's constructorfrom the catch handler as thepdb 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 atry block areinaccessible from its associated catch handlers, object data members wereavailable before starting thetry block and so they may be accessed from acatch handler. In the following example the catch handler inPersonDb's constructor is able to access itsd_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. Theinitialize member isunable to reassignd_data andd_size ifPersonDb const pdbwas defined; theinitialize member should at least offer the basicexception guarantee and return any resources it has acquired beforeterminating due to a thrown exception; and althoughd_data andd_sizeoffer the nothrow guarantee as they are of primitive data types a class typedata member might throw an exception, possibly resulting in violation of thebasic guarantee.

In the next implementation ofPersonDb assume that constructorreceives a pointer to an already allocated block ofPerson objects. ThePersonDb object takes ownership of the allocated memory and it istherefore responsible for the allocated memory's eventual destruction.Moreover,d_data andd_size are also used by a composed objectPersonDbSupport, having a constructor expecting aPerson const * andsize_t argument. Our next implementation may then look something likethis:

    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 aPersonDb const &pdb. Unfortunately,PersonDb cannot offer the basic guarantee. IfPersonDbSupport'sconstructor throws an exception it isn't caught althoughd_data alreadypoints to allocated memory.

Thefunction try block offers a solution for this problem. A functiontry block consists of atry block and its associated handlers. Thefunctiontry block startsimmediately after the function header, andits block defines the function body. With constructors base class and datamember initializers may be placed between thetry keyword and the openingcurly brace. Here is our final implementation ofPersonDb, now offeringthe 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 afunction try block. The exception thrown by theThrow object is initiallycaught by the object itself. Then it is rethrown. The surroundingComposer's constructor also defines a function try block,Throw'srethrown exception is properly caught byComposer's exception handler,even though the exception was generated from within its member initializerlist:

    #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 runsand then breaks with anabort 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 theC++ standard: at the end of acatch-handler belonging to a constructor or destructor function try block, theoriginal 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. Theexception is only rethrown if it reaches the end of the catch handler of aconstructor or destructor function try block. Exceptions caught by nestedcatch handlers are not automatically rethrown.

As only constructors and destructors rethrow exceptions caught by theirfunction try block catch handlers the run-time error encountered in the aboveexample may simply be repaired by providingmain with its own function tryblock:

    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 anexception throw list then only the types of rethrown exceptions must matchthe types mentioned in the throw list.

10.12: Exceptions in constructors

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

The following example illustrates this situation in its prototypicalform. The constructor of the classIncomplete first displays a messageand 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 anIncomplete object inside atryblock. 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, ifIncomplete's constructor would actually have allocated somememory, the program would suffer from a memory leak. To prevent this fromhappening, the following counter measures are available:

The catch clause of a constructor's functiontry block behavesslightly different than a catch clause of an ordinary functiontryblock. An exception reaching a constructor's functiontry block may betransformed into another exception (which is thrown from the catch clause) butif no exception is explicitly thrown from the catch clause the exceptionoriginally reaching the catch clause is always rethrown. Consequently, there'sno way to confine an exception thrown from a base class constructor or from amember initializer to the constructor: such an exceptionalways propagatesto a more shallow block and in that case the object's construction is alwaysconsidered incomplete.

Therefore, if incompletely constructed objects throw exceptions thenthe constructorremains responsible for preventing memory(generally: resource) leaks. There are several ways to realize this:

On the other hand,C++ supports constructor delegation, so an objectmay have been completely constructed according to theC++ run-time system,but yet its (delegating) constructor may throw an exception, as illustrated bythe next example:

     1: #include <iostream>     2: using namespace std;     3:      4: class Delegate     5: {     6:     char *d_p1;     7:     char *d_p2;     8:      9:     public:    10:         Delegate()      // succeeds -> object constructed    11:         :    12:             Delegate(0)    13:         {    14:             d_p2 = new char[10];    15:             cout << "default, throws...\n";    16:             throw 12;   // but considered constructed    17:         }    18:         ~Delegate()    19:         {    20:             delete[] d_p1;    21:             delete[] d_p2;    22:             cout << "destructor\n";    23:         }    24:     25:     private:    26:         Delegate(int x)         // completes OK    27:         :    28:             d_p1(0),    29:             d_p2(0)    30:         {    31:             cout << "delegated\n";    32:         }    33: };    34:     35: int main()    36: try    37: {    38:     Delegate del;           // throws    39:     40:     cout << "never reached\n";    41: } // del's destructor is called here    42: catch (...)    43: {    44:     cout << "main's catch clause\n";    45: }

Here it is the responsibility ofDelegate's designer to ensure thatthe throwing default constructor does not invalidate the actions performed bytheDelegate(int x) constructor. The latter constructor is called (line12) by the default constructor, and merely initializes (lines 28, 29) the datamembers at lines 6 and 7. Next, the default constructor, after allocating somememory, throws an exception (line 16). In fact, an exception may be called atany point, since the destructor (line 18) will be called automatically anyway(line 41). If multiple exceptions could be thrown thenDelegate can definean enumeration and a data member of that enumeration type, which is set to theenum value indication the nature of the next exception (if it is thrown), sothe destructor can handle the exception according to its type.

10.13: Exceptions in destructors

According to theC++ standard exceptions thrown by destructors maynot leave their bodies. Providing a destructor with a functiontryblock is therefore a violation of the standard: exceptions caught by afunctiontry block's catch clause have already left the destructor's body.If --in violation of the standard-- the destructoris provided with afunctiontry block and an exception is caught by thetry block thenthat exception is rethrown, similar to what happens in catch clauses ofconstructor functions'try blocks.

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

Assume a carpenter builds a cupboard having a single drawer. The cupboardis finished, and a customer, buying the cupboard, finds that the cupboard canbe used as expected. Satisfied with the cupboard, the customer asks thecarpenter to build another cupboard, this time havingtwodrawers. When the second cupboard is finished, the customer takes it home andis utterly amazed when the second cupboard completely collapses immediatelyafter 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 finalAbort indicates that the program has aborted instead ofdisplaying a message likeCupboard2 behaves as expected.

Let's have a look at the three classes involved. The classDrawer has noparticular 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 classCupboard1 has no special characteristics at all. It merelyhas a single composedDrawer object:
    class Cupboard1    {        Drawer left;        public:            Cupboard1()            :                left(1)            {}    };
The classCupboard2 is constructed comparably, but it has twocomposedDrawer objects:
    class Cupboard2    {        Drawer left;        Drawer right;        public:            Cupboard2()            :                left(1),                right(2)            {}    };

WhenCupboard1's destructor is calledDrawer's destructor iseventually called to destroy its composed object. This destructor throws anexception, which is caught beyond the program's firsttry block. Thisbehavior is completely as expected.

A subtlety here is thatCupboard1's destructor (and henceDrawer'sdestructor) is activatedimmediately subsequent to its construction. Itsdestructor is called immediately subsequent to its construction asCupboard1() defines an anonymous object. As a result theBeyondCupboard1 object text is never inserted intostd::cerr.

Because ofDrawer's destructor throwing an exception a problem occurswhenCupboard2's destructor is called. Of its two composed objects, thesecondDrawer's destructor is called first. This destructor throws anexception, which ought to be caught beyond the program's secondtryblock. However, although the flow of control by then has left the context ofCupboard2's destructor, that object hasn't completely been destroyed yetas 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 fromCupboard2's destructor any remaining actions would simply be ignored,albeit that (as both drawers are properly constructed objects)left'sdestructor would still have to be called.

This happens here too andleft's destructoralso needs to throw anexception. But as we've already left the context of the secondtry block,the current flow control is now thoroughly mixed up, and the program has noother option but to abort. It does so by callingterminate(), which inturn callsabort(). Here we have our collapsing cupboard having twodrawers, even though the cupboard having one drawer behaves perfectly.

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

TheC++ standard therefore understandably stipulates that exceptionsmaynever leave destructors. Here is the skeleton of a destructor whose code might throwexceptions. No functiontry block but all the destructor's actions areencapsulated in atry block nested under the destructor's body.

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




[8]ページ先頭

©2009-2025 Movatter.jp