exit to terminate the program completely. A tough way to handle a
problem if only because the destructors of local objects aren't activated.
setjmp and
longjmp to enforce non-local exits. This mechanism implements a kind
of goto jump, allowing the program to continue at an outer level,
skipping the intermediate levels which would have to be visited if a series of
returns from nested functions would have been used.
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.
throw statement. The keyword
throw, followed by an expression of a certain type, throws the expression
value as an exception. In C++ anything having value semantics may be
thrown as an exception: an int, a bool, a string, etc. However,
there also exists a standard exception type (cf. section 10.8) that
may be used as base class (cf. chapter 13) when defining new
exception types.
try-block. The run-time support system ensures that all of the
program's code is itself surrounded by a global try block. Thus, every
exception generated by our code will always reach the boundary of at least one
try-block. A program terminates when an exception reaches
the boundary of the global try block, and when this happens destructors of
local and global objects that were alive at the point where the exception was
generated are not called. This is not a desirable situation and therefore all
exceptions should be generated within a try-block explicitly defined by
the program. Here is an example of a string exception thrown from within a
try-block:
try
{
// any code can be defined here
if (someConditionIsTrue)
throw "this is the std::string exception"s;
// any code can be defined here
}
catch: Immediately following the try-block, one or more
catch-clauses must be defined. A catch-clause consists of a
catch-header defining the type of the exception it can catch followed by a
compound statement defining what to do with the caught exception:
catch (string const &msg)
{
// statements in which the caught string object are handled
}
Multiple catch clauses may appear underneath each other, one for each
exception type that has to be caught. In general the catch clauses may
appear in any order, but there are exceptions requiring a specific order. To
avoid confusion it's best to put a catch clause for the most general
exception last. At most one exception clause will be activated. C++
does not support a Java-style finally-clause activated after
completing a catch clause.
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.
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.
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?
return-statements, and
without the need to terminate the program using blunt tools like the function
exit.
setjmp and longjmp do distrupt the proper activation of
destructors their use is strongly deprecated in C++.
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:
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 << "';
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{};
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.
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:
try-block no exception
handler is activated, and execution continues from the last statement in
the try-block to the first statement beyond the last catch-block.
try-block but neither
the current level nor another level contains an appropriate exception handler,
the program's default exception handler is called, aborting the program.
try-block and an appropriate
exception handler is available, then the code of that exception handler is
executed. Following that, the program's execution continues at the first
statement beyond the last catch-block.
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.
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 10.9.4 for further coverage of the
class std::exception_ptr.
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 noexept there's a slight run-time overhead penalty because the
function needs an over-all try-catch block catching any exception 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.
ios::exceptions member function. This function has two overloaded
versions:
ios::iostate exceptions():void exceptions(ios::iostate state)state is observed.
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.
<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:
std::bad_alloc (this requires the <new> header file):
thrown when operator new fails;
std::bad_array_new_length (this requires the
<new> header file): thrown when an illegal array size is requested
when using new Type[...]. Illegal sizes are negative values,
values that exceed an implementation defined maximum, the number of
initializer clauses exceeds the specified number of array elements
(e.g., new int[2]{ 1, 2, 3 });
std::bad_cast (this requires the <typeinfo> header
file): thrown in the context of polymorphism
(see section 14.6.1);
std::bad_exception (this requires the
<exception> header file): thrown when a function tries to
generate another type of exception than declared in its
function throw list;
std::bad_typeid (this requires the <typeinfo> header
file): also thrown in the context of
polymorphism (see section 14.6.2);
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:
std::domain_error: a (mathematical) domain error is
detected;
std::invalid_argument: the argument of a function
has an invalid value;
std::length_error: thrown when an object would have
exceeded its maximum permitted length;
std::logic_error: a logic error should be thrown when a
problem is detected in the internal logic of the program. Example: a function
like C's printf is called with more arguments than there are format
specifiers in its format string;
std::out_of_range: thrown when an argument exceeds its
permitted range. Example: thrown by at members when their arguments exceed
the range of admissible index values;
std::overflow_error: an overflow error should be
thrown when an arithmetic overflow is detected. Example: dividing a
value by a very small value;
std::range_error: a range error should be thrown when
an internal computation results in a value exceeding a permissible range;
std::runtime_error: a runtime error should be thrown
when a problem is encountered that can only be detected while the program is
being executed. Example: a non-integral is entered when the program's input
expects an integral value.
std::underflow_error: an underflow error should be
thrown when an arithmetic underflow is detected. Example: dividing a very
small value by a very large value.
std::tx_exception<Type>: derived from
std::runtime_error. This exception can be thrown from an atomic_cancel
compound statement (cf. section 20.14) to undo statements executed so
far.
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.
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.1) 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.
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.
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:
virtual error_condition
default_error_condition(int ev) const noexcept:error_condition object (cf. section 10.9.2)
initialized with error value ev and the current
(i.e., *this) error_category;
virtual bool equivalent(error_code const &code, int condition) const
noexcept:true if the equivalence between
the error condition that is associated with the error_code object
and the error_condition_enum value that is specified (as an
int value) as the function's second argument could be
establisted;
virtual bool equivalent(int ev, error_condition const &condition)
const noexcept:true if the equivalence of an error_condition object
that is constructed from the ErrorConditionEnum value that is
associated with the ErrorCategoryEnum value that was passed (as
int) to the function and the error_condition object that was
passed to the function as its second argument could be established;
virtual string message(int ev) const:ev, which should be a (cast to int) value of the category's
error condition enumeration;
virtual char const *name() const noexcept:generic);
bool operator<(error_category const &rhs) const noexcept:less<const error_category*>()(this, &rhs).
The functions returning predefined error categories are:
error_category const &generic_category() noexcept:error_category object.
The returned object's name member returns a pointer to the string
"generic";
error_category const &system_category() noexcept:error_category
object: it is used for errors reported by the operating system. The
object's name member returns a pointer to the string
"system";
error_category const &iostream_category() noexcept:error_category
object: it is used for errors reported by stream objects. The
object's name member returns a pointer to the string
"iostream";
error_category const &future_category() noexcept:error_category object: it is
used for errors reported by `future' objects (cf. section
20.8). The object's name member returns a pointer to the
string "future";
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:
error_condition() noexcept:system_category error category;
error_condition(int ec, error_category const &cat) noexcept:ec and error category
cat. It is the responsibility of the caller to ensure that ec
represents a (cast to int) value of cat's error condition
enumeration;
error_condition(ErrorConditionEnum value) noexcept:template <class ErrorConditionEnum>. It initializes the
object with the return value of
make_error_condition(value);
Members:
ErrorConditionEnum are available;
void assign(int val, error_category const &cat):
error_category const &category() const noexcept:
void clear() noexcept:generic_category;
string message() const:category().message(value());
explicit operator bool() const noexcept:true if value() returns a non-zero value (so its
semantic meaning is `the object represents an error');
int value() const noexcept:
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.
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).
std::current_exception), and access to any exception
can be standardized using std::make_exception_ptr.
These functions expect or use objects of the class
std::exception_ptr, and in this section we take a closer
look at tha class.
The class exception_ptr's default constructor initializes it to a
null-pointer. In the following code snippet the variable isNull is set to
true:
std::exception_ptr obj;
bool isNull = obj == nullptr && obj == 0;
The class exception_ptr provides copy and move constructors as well as
copy and move assignment operators.
Two exception_ptr objects can be compared for equality. They are equal
if they refer to the same exception. Move assignment transfers the exception
referred to by the right-hand side operand to the left-hand side operand, and
turns the right-hand side operand into a null pointer.
There is no published method directly retrieving the exception to which an
exception_ptr object refers. However, there are some free functions
constructing or handling exception_ptr objects:
std::exception_ptr std::current_exception() noexcept:exception_ptr object is returned referring to the currently
handled exception (or a copy of the currently handled exception, or a
default constructed exception_ptr object if no current exception
is available). This function can also be called when a default
exception catcher is used.
The exception referred to by current_exception does not have to
be an object of the class std::exception. Any type of object or
value thrown as an exception is retrieved as an exception_ptr by
current_exception. The exception referred to by an
exception_ptr object remains valid for at least as long as there
exists an exception_ptr object that refers to it. Calling
current_exception twice in a row then the two returned
exception_ptr objects may or may not refer to the same exception
object.
std::exception_ptr make_exception_ptr(Type
value) noexcept:exception_ptr from a value of
any type which is passed as its argument. Type does not
necessarily have to be a std::exception but can be anything that
can be thrown as an exception: an int, a std::string, a
std::exception, you name it.
Here are
some examples, showing how values of different types can be passed as
arguments to make_exception_ptr
auto ptr = make_exception_ptr(exception());
ptr = make_exception("hello world"s);
ptr = make_exception(12);
void std::rethrow_exception(exception_ptr obj):obj refers is thrown. Note: obj
cannot be a nullptr.
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:
X x: the default constructor could throw an exception (#1)
cout << x: the overloaded insertion operator could throw an
exception (#2), but its rhs argument might not be an X but, e.g., an
int, and so X::operator int() const could be called which offers yet
another opportunity for an exception (#3).
*xp = new X{ x }: the copy constructor may throw an exception
(#4) and operator new (#5a) too. But did you realize that this latter
exception might not be thrown from ::new, but from, e.g., X's own
overload of operator new? (#5b)
cout << (x + *xp): we might be seduced into thinking that two
X objects are added. But it doesn't have to be that way. A separate class
Y might exist and X may have a conversion operator operator Y() const,
and operator+(Y const &lhs, X const &rhs), operator+(X const &lhs, Y const
&rhs), and operator+(X const &lhs, X const &rhs) might all exist. So, if
the conversion operator exists, then depending on the kind of overload of
operator+ that is defined either the addition's left-hand side operand
(#6), right-hand side operand (#7), or operator+ itself (#8) may throw an
exception. The resulting value may again be of any type and so the overloaded
cout << return-type-of-operator+ operator may throw an exception
(#9). Since operator+ returns a temporary object it is destroyed shortly
after its use. X's destructor could throw an exception (#10).
delete xp: whenever operator new is overloaded operator
delete should be overloaded as well and may throw an exception (#11). And of
course, X's destructor might again throw an exception (#12).
}: when the function terminates the local x object is
destroyed: again an exception could be thrown (#13).
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:
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.
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).
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.
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.
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:
try block, allowing the exception to be caught
by the constructor itself. This approach is defensible when the constructor
is able to repair the cause of the exception and to complete its construction
as a valid object.
try block within the constructor's
body won't be able to catch the thrown exception. This always results in
the exception leaving the constructor and the object is not considered to have
been properly constructed. A try block may include the member
initializers, and the try block's compound statement becomes the
constructor's body as in the following example:
class Incomplete2
{
Composed d_composed;
public:
Incomplete2()
try
:
d_composed(/* arguments */)
{
// body
}
catch (...)
{}
};
An exception thrown by either the member initializers or the body results in the execution never reaching the body's closing curly brace. Instead the catch clause is reached. Since the constructor's body isn't properly completed the object is not considered properly constructed and eventually the object's destructor won't be called.
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.
Therefore, if incompletely constructed objects throw exceptions then the constructor remains responsible for preventing memory (generally: resource) leaks. There are several ways to realize this:
shared_ptr
objects are, after all, objects.
class Incomplete2
{
Composed d_composed;
char *d_cp; // plain pointers
int *d_ip;
public:
Incomplete2(size_t nChars, size_t nInts)
try
:
d_composed(/* arguments */), // might throw
d_cp(0),
d_ip(0)
{
try
{
preamble(); // might throw
d_cp = new char[nChars]; // might throw
d_ip = new int[nChars]; // might throw
postamble(); // might throw
}
catch (...)
{
delete[] d_cp; // clean up
delete[] d_ip;
throw; // retrow the exception
}
}
catch (...)
{
// maybe write a log-entry, but also throws
// the original exception
}
};
On the other hand, C++ supports constructor delegation, so an object may have been completely constructed according to the C++ run-time system, but yet its (delegating) constructor may throw an exception, as illustrated by the 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 of Delegate's designer to ensure that
the throwing default constructor does not invalidate the actions performed by
the Delegate(int x) constructor. The latter constructor is called (line
12) by the default constructor, and merely initializes (lines 28, 29) the data
members at lines 6 and 7. Next, the default constructor, after allocating some
memory, throws an exception (line 16). In fact, an exception may be called at
any point, since the destructor (line 18) will be called automatically anyway
(line 41). If multiple exceptions could be thrown then Delegate can define
an enumeration and a data member of that enumeration type, which is set to the
enum value indication the nature of the next exception (if it is thrown), so
the destructor can handle the exception according to its type.
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 (...)
{}
}