Don't hesitate to send in feedback: send an e-mail if you like the C++ Annotations; if you think that important material was omitted; if you find errors or typos in the text or the code examples; or if you just feel like e-mailing. Send your e-mail to Frank B. Brokken.Please state the document version you're referring to, as found in the title (in this document: 6.5.0) and please state chapter and paragraph name or number you're referring to.
All received mail is processed conscientiously, and received suggestions for improvements will usually have been processed by the time a new version of the Annotations is released. Except for the incidental case I will normally not acknowledge the receipt of suggestions for improvements. Please don't interpret this as me not appreciating your efforts.
C supports several ways in which a program can react to situations which break the normal unhampered flow of the program:
exit()
to terminate the program completely. A tough way to handle a
problem....
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()
approach isn't frequently seen 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 setjmp()
and longjmp()
are. 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 and their syntax will be introduced. First an
example of the different impacts exceptions and setjmp()
and longjmp()
have on a program will be given. Then the discussion will dig into the
formalities exceptions.
try
: The try
-block surrounds statements in which exceptions may
be generated (the parlance is for exceptions to be thrown). Example:
try { // statements in which exceptions may be thrown }
throw
: followed by an expression of a certain type, throws the value
of the expression as an exception. The throw
statement must be executed
somewhere within the try
-block: either directly or from within a function
called directly or indirectly from the try
-block. Example:
throw "This generates a char * exception";
catch
: Immediately following the try
-block, the catch
-block
receives the thrown exceptions. Example of a catch
-block receiving
char *
exceptions:
catch (char *message) { // statements in which the thrown char * exceptions are handled }
Outer
and Inner
. An Outer
object is created in
main()
, and its member Outer::fun()
is called. Then, in
Outer::fun()
an Inner
object is constructed. Having
constructing the Inner
object, its member Inner::fun()
is called.
That's about it. The function Outer::fun()
terminates, and the destructor
of the Inner
object is called. Then the program terminates and the
destructor of the Outer
object is called. Here is the basic program:
#include <iostream> using namespace std; class Inner { public: Inner(); ~Inner(); void fun(); }; class Outer { public: Outer(); ~Outer(); void fun(); }; Inner::Inner() { cout << "Inner constructor\n"; } Inner::~Inner() { cout << "Inner destructor\n"; } void Inner::fun() { cout << "Inner fun\n"; } 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, and it shows exactly what we want: 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 to take place in the Inner::fun()
function,
which is supposedly handled somewhere at the end of the function main()
.
We'll consider two variants. The first variant will try to handle this
situation using
setjmp()
and
longjmp()
; the second variant will try to
handle this situation using C++'s
exception mechanism.
setjmp()
and
longjmp()
the basic program from section
8.2 is slightly modified to contain a variable
jmp_buf jmpBuf
. The function Inner::fun()
now calls longjmp
,
simulating a
disastrous event, to be handled at the end of the function
main()
. In main()
we see the standard code defining the target
location of the long jump, using the function setjmp()
. A zero return
value indicates the initialization of the jmp_buf
variable, upon which the
Outer::fun()
function is called. This situation represents the `normal
flow'.
To complete the simulation, the return value of the program is zero only if
the program is able to return from the function Outer::fun()
normally. However, as we know, this won't happen: Inner::fun()
calls
longjmp()
, returning to the setjmp()
function, which (at this time)
will not return a zero return value. Hence, after calling Inner::fun()
from Outer::fun()
the program proceeds beyond the if
-statement in the
main()
function, and the program terminates with the return value 1. Now
try to follow these steps by studying the following program source, modified
after the basic program given in section 8.2:
#include <iostream> #include <setjmp.h> #include <cstdlib> using namespace std; class Inner { public: Inner(); ~Inner(); void fun(); }; class Outer { public: Outer(); ~Outer(); void fun(); }; jmp_buf jmpBuf; Inner::Inner() { cout << "Inner constructor\n"; } void Inner::fun() { cout << "Inner fun()\n"; longjmp(jmpBuf, 0); } Inner::~Inner() { cout << "Inner destructor\n"; } 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)) { out.fun(); return 0; } return 1; } /* Generated output: Outer constructor Inner constructor Outer fun Inner fun() Outer destructor */The output produced by this program clearly shows that the destructor of the class
Inner
is not executed. This is a direct result of the
non-local characteristic of the call to longjmp()
: processing proceeds
immediately from the longjmp()
call in the member function
Inner::fun()
to the function setjmp()
in main()
. There, its return
value is zero, so the program terminates with return value 1. What is
important here is that the call to the destructor Inner::~Inner()
, waiting
to be executed at the end of Outer::fun()
, is never reached.
As this example shows that the destructors of objects can easily be skipped
when
longjmp()
and
setjmp()
are used, these function should be avoided completely in C++ programs.
setjmp()
and
longjmp()
. In this section an example
using exceptions is presented. Again, the program is derived from the basic
program, given in section 8.2:
#include <iostream> using namespace std; class Inner { public: Inner(); ~Inner(); void fun(); }; class Outer { public: Outer(); ~Outer(); 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"; } 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 (...) {} } /* Generated output: Outer constructor Inner constructor Outer fun Inner fun Inner destructor Outer destructor */In this program an exception is thrown, where a
longjmp()
was
used in the program in section 8.2.1. The comparable construct for
the setjmp()
call in that program is represented here by the
try
and
catch
blocks. The try
block surrounds statements (including function
calls) in which exceptions are thrown, the catch
block may contain
statements to be executed just after throwing an exception.
So, comparably to the example given in section 8.2.1, the
function Inner::fun()
terminates, albeit with an exception rather than by
a call to longjmp()
. The exception is caught in main()
, and the
program terminates. When the output from the current program is inspected, we
notice that the destructor of the Inner
object, created in
Outer::fun()
is now correctly called. Also notice that the execution of
the function Inner::fun()
really terminates at the throw
statement:
the insertion of the text into cout
, just beyond the throw
statement,
doesn't take place.
Hopefully this has raised your appetite for exceptions, since it was shown that:
return
-statements, and without the need
to terminate the program.
setjmp()
and
longjmp()
.
throw
statement. The throw
keyword is
followed by an expression, resulting in a value of a certain type. For
example:
throw "Hello world"; // throws a char * throw 18; // throws an int throw string("hello"); // throws a stringObjects defined locally in functions are automatically destroyed once exceptions thrown by these functions leave these functions. However, if the object itself is thrown, the exception catcher receives a copy of the thrown object. This copy is constructed just before the local object is destroyed.
The next example illustrates this point. Within the function
Object::fun()
a local Object toThrow
is created, which is thereupon
thrown as an exception. The exception is caught outside of Object::fun()
,
in main()
. At this point the thrown object doesn't actually exist anymore,
Let's first take a look at the sourcetext:
#include <iostream> #include <string> using namespace std; class Object { string d_name; public: Object(string name) : d_name(name) { cout << "Object constructor of " << d_name << "\n"; } Object(Object const &other) : d_name(other.d_name + " (copy)") { cout << "Copy constructor for " << d_name << "\n"; } ~Object() { cout << "Object destructor of " << d_name << "\n"; } void fun() { Object toThrow("'local object'"); cout << "Object 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(); } } /* Generated output: Object constructor of 'main object' Object constructor of 'local object' Object fun() of 'main object' Copy constructor for 'local object' (copy) Object destructor of 'local object' Copy constructor for 'local object' (copy) (copy) Caught exception Hello by 'local object' (copy) (copy) Object destructor of 'local object' (copy) (copy) Object destructor of 'local object' (copy) Object destructor of 'main object' */The class
Object
defines several simple constructors and members. The
copy constructor is special in that it adds the text " (copy)"
to the
received name, to allow us to monitor the construction and destruction of
objects more closely. The member function Object::fun()
generates the
exception, and throws its locally defined object. Just before the exception
the following output is generated by the program:
Object constructor of 'main object' Object constructor of 'local object' Object fun() of 'main object'Now the exception is generated, resulting in the next line of output:
Copy constructor for 'local object' (copy)The
throw
clause receives the local object, and treats it as a value
argument: it creates a copy of the local object. Following this, the exception
is processed: the local object is destroyed, and the catcher catches an
Object
, again a
value parameter. Hence, another copy is
created. Threfore, we see the following lines:
Object destructor of 'local object' Copy constructor for 'local object' (copy) (copy)Now we are inside the catcher, who displays its message:
Caught exceptionfollowed by the calling of the
hello()
member of the received
object. This member also shows us that we received a copy of the copy of
the local object of the Object::fun()
member function:
Hello by 'local object' (copy) (copy)Finally the program terminates, and its still living objects are now destroyed in their reversed order of creation:
Object destructor of 'local object' (copy) (copy) Object destructor of 'local object' (copy) Object destructor of 'main object'
If the catcher would have been implemented so as to receive a
reference to an object (which you could do by using `catch (Object
&o)
'), then repeatedly calling the copy constructor would have been
avoided. In that case the output of the program would have been:
Object constructor of 'main object' Object constructor of 'local object' Object fun() of 'main object' Copy constructor for 'local object' (copy) Object destructor of 'local object' Caught exception Hello by 'local object' (copy) Object destructor of 'local object' (copy) Object destructor of 'main object'This shows us that only a single copy of the local object has been used.
Of course, 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 dies once the exception is thrown, and the catcher receives a wild pointer. Bad news....
Summarizing:
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";The location of these
throw
statements is immaterial: they may be placed deeply nested within the program,
or at a more superficial level. Furthermore, functions may be used to
generate
the expression which is then
thrown. A function
char const *formatMessage(char const *fmt, ...);would allow us to throw more specific messages, like
if (!lookup(variableName)) throw formatMessage("Variable '%s' not defined", 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 code
handling an exception the received exception is passed on to the next level
that might be able to process that particular type of exception.
In our server-client situation a function
initialExceptionHandler(char *exception)could be designed to do so. The received message is inspected. If it's a simple message it's processed, otherwise the exception is passed on to an outer level. The implementation of
initialExceptionHandler()
shows the
empty throw
statement:
void initialExceptionHandler(char *exception) { if (!plainMessage(exception)) throw; handleTheMessage(exception); }As we will see below (section 8.5), the empty
throw
statement passes on the exception received in a catch
-block. Therefore, a
function like initialExceptionHandler()
can be used for a variety of
thrown exceptions, as long as the argument used with
initialExceptionHandler()
is compatible with the nature of the received
exception.
Does this sound intriguing? Then try to follow the next example, which jumps slightly ahead to the topics covered in chapter 14. The next example may be skipped, though, without loss of continuity.
We can now state that a
basic exception handling class can be constructed
from which specific exceptions are derived. Suppose we have a class
Exception
, containing a member function ExceptionType
Exception::severity()
. This member function tells us (little wonder!) the
severity of a thrown exception. It might be Message, Warning, Mistake,
Error
or Fatal
. Furthermore, depending on the severity, a thrown
exception may contain less or more information, somehow processed by a
function process()
. In addition to this, all exceptions have a plain-text
producing member function, e.g.,
toString()
, telling us a bit more about
the nature of the generated exception.
Using
polymorphism, process()
can be made to behave differently,
depending on the nature of a thrown exception, when called through a basic
Exception
pointer or reference.
In this case, a program may throw any of these five types of exceptions. Let's
assume that the Message
and Warning
exceptions are processable by our
initialExceptionHandler()
. Then its code would become:
void initialExceptionHandler(Exception const *e) { cout << e->toString() << endl; // show the plain-text information if ( e->severity() != ExceptionWarning && e->severity() != ExceptionMessage ) throw; // Pass on other types of Exceptions e->process(); // Process a message or a warning delete e; }Due to polymorphism (see chapter 14),
e->process()
will
either process a Message
or a Warning
. Thrown exceptions are generated
as follows:
throw new Message(<arguments>); throw new Warning(<arguments>); throw new Mistake(<arguments>); throw new Error(<arguments>); throw new Fatal(<arguments>);All of these exceptions are processable by our
initialExceptionHandler()
, which may decide to pass exceptions upward for
further processing or to process exceptions itself. The polymorphic exception
class is developed further in section 14.7.
try
-block surrounds statements in which exceptions may be thrown. As
we have seen, the actual throw
statement can be placed everywhere, not
necessarily directly in the try
-block. It may, for example, be placed in a
function, called from within the try
-block.
The keyword try
is followed by a set of curly braces, acting like a
standard C++
compound statement: multiple statements and
definitions may be placed here.
It is possible (and very common) to create
levels in
which exceptions may be thrown. For example, main()
's code is surrounded
by a try
-block, forming an outer level in which exceptions can be handled.
Within main()
's try
-block, functions are called which may also contain
try
-blocks, forming the next level in which exceptions may be
generated. As we have seen (in section 8.3.1), exceptions thrown in
inner level try
-blocks may or may not be processed at that level. By
placing an
empty throw
in an exception handler, the
thrown exception is passed on to the next (outer) level.
If an exception
is thrown outside of any try
-block, then the default
way to handle (uncaught) exceptions is
used, which is normally to
abort the program. Try to compile and run the
following tiny program, and see what happens:
int main() { throw "hello"; }
catch
block contains code that is executed when an exception is
thrown. Since expressions are thrown, the catch
-block must know what
kind of exceptions it should be able to handle. Therefore, the keyword
catch
is followed by a parameter list consisting of but one parameter,
which is the type of the exception handled by the catch
block. So, an exception handler for char const *
exceptions will have the
following form:
catch (char const *message) { // code to handle the message }Earlier (section 8.3) we've seen that such a message doesn't have to be thrown as a static string. It's also possible for a function to return a string, which is then thrown as an exception. If such a function creates the string that is thrown as an exception dynamically, the exception handler will normally have to delete the allocated memory to prevent a memory leak.
Close attention should be paid to the nature of the parameter of the exception handler, to make sure that dynamically generated exceptions are deleted once the handler has processed them. Of course, when an exception is passed on to an outer level exception handler, the received exception should not be deleted by the inner level handler.
Different kinds of exceptions may be thrown: char *
s, int
s, pointers
or references to objects, etc.: all these different types may be used in
throwing and catching exceptions. So, various types of exceptions may come out
of a try
-block. In order to
catch all expressions that may emerge from
a try
-block, multiple exception handlers (i.e., catch
-blocks) may
follow the try
-block.
To some extent 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. So only one exception handler following a
try
-block will be executed. Normally this is no problem: the thrown
exception is of a certain type, and the correspondingly typed catch-handler
will catch it. For example, if exception handlers are defined for
char *
s and
void *
s then ASCII-Z strings will be caught by the latter
handler. Note that a char *
can also be considered a void *
, but even
so, an ASCII-Z string will be handled by a char *
handler, and not by a
void *
handler. This is true in general: 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
hierarchical relation to each other (i.e., int
is not derived from
double
; string
is not derived from ASCII-Z):
#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"; } } }As an alternative to constructing different types of exception handlers for different types of exceptions, a specific class can be designed whose objects contain information about the exception. Such an approach was mentioned earlier, in section 8.3.1. Using this approach, there's only one handler required, since we know we won't throw other types of exceptions:
try { // code throws only Exception pointers } catch (Exception *e) { e->process(); delete e; }The
delete e
statement in the above code indicates
that the Exception
object was created dynamically.
When the code of an exception handler has been processed, execution continues
beyond the last exception handler directly following that try
-block
(assuming the handler doesn't itself use flow control statements (like
return
or throw
) to break the default flow of execution). From this,
we distinguish the following
cases:
try
-block no exception
handler is activated, and the 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 an other level contains an appropriate exception
handler, the program's
default exception handler is called, usually
aborting the program.
try
-block and an appropriate
exception handler is available, then the code of that exception handler is
executed. Following the execution of the code of the exception handler, the
execution of the program continues at the first statement beyond the last
catch
-block.
try
block appearing below an executed
throw
-statement will be
ignored. However, destructors
of objects defined
locally in the try
-block are called, and they are called before any
exception handler's code is executed.
The actual
computation or construction of an
exception may be realized using various degrees of sophistication. For
example, it's possible to use the operator new
; to use static member
functions of a class; to return a pointer to an object; or to use objects of
classes derived from a class, possibly involving polymorphism.
An intermediate
kind of exception handling may be
implemented using the
default exception handler, which should (due to the
hierarchical nature of exception catchers, discussed in section
8.5) be placed beyond all other, more specific exception
handlers. In this case, the current level of exception handling may do some
processing by default, but will then, using the the
empty
throw
statement (see section 8.3.1), pass the thrown exception
on to an outer level. Here is an example showing the use of a default
exception handler:
#include <iostream> using namespace std; int main() { try { try { throw 12.25; // no specific handler for doubles } catch (char const *message) { cout << "Inner level: caught char const *\n"; } 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 still knows the double: " << d << endl; } } /* Generated output: Inner level: generic handling of exceptions Outer level still knows the double: 12.25 */From the generated output we may conclude that an empty
throw
statement throws the received exception to the next (outer) level of exception
catchers, keeping the type and value of the exception: basic or generic
exception handling can thus be accomplished at an inner level, specific
handling, based on the type of the thrown expression, can then continue at an
outer level.
These external functions may of course throw exceptions. Declarations of
such functions may contain a
function throw list or
exception specification list, in which the types of the exceptions
that can be thrown by the function are specified. For example, a function that
could throw `char *
' and `int
' exceptions can be declared as
void exceptionThrower() throw(char *, int);If specified, a function throw list appears immediately beyond the function header (and also beyond a possible
const
specifier), and,
noting that throw lists may be empty, it has the following generic form:
throw([type1 [, type2, type3, ...]])
If a function doesn't throw exceptions an empty function throw list may be used. E.g.,
void noExceptions() throw ();In all cases, the function header used in the function definition must exactly match the function header that is used in the declaration, e.g., including a possible empty function throw list.
A function for which a function throw list is specified may not throw other types of exceptions. A run-time error occurs if it tries to throw other types of exceptions than those mentioned in the function throw list.
For example, consider the declarations and definitions in the following program:
#include <iostream> using namespace std; void charPintThrower() throw(char const *, int); // declarations class Thrower { public: void intThrower(int) const throw(int); }; void Thrower::intThrower(int x) const throw(int) // definitions { if (x) throw x; } void charPintThrower() throw(char const *, int) { int x; cerr << "Enter an int: "; cin >> x; Thrower().intThrower(x); throw "this text is thrown if 0 was entered"; } void runTimeError() throw(int) { throw 12.5; } int main() { try { charPintThrower(); } catch (char const *message) { cerr << "Text exception: " << message << endl; } catch (int value) { cerr << "Int exception: " << value << endl; } try { cerr << "Up to the run-time error\n"; runTimeError(); } catch(...) { cerr << "not reached\n"; } }In the function
charPintThrower()
the throw
statement clearly
throws a char const *
. However, since intThrower()
may throw an
int
exception, the function throw list of charPintThrower()
must
also contain int
.
If the function throw list is not used, the function may either throw exceptions (of any kind) or not throw exceptions at all. Without a function throw list the responsibility of providing the correct handlers is in the hands of the program's designer.
ios::exceptions()
member function. This function has two overloaded
versions:
iostate exceptions()
: this member returns the
state flags for which the stream will throw exceptions,
void exceptions(iostate state)
: this member will throw an exception
when state
state
is observed.
ios::failure
, derived from
ios::exception
. A failure
object can be
constructed with a string const &message
, which can be retrieved using the
virtual char const *what() const
member.
Exceptions should be used for exceptional situations. Therefore, we think
it is questionable to have stream objects throw exceptions for rather standard
situations like EOF
. Using exceptions to handle input errors might be
defensible, for example when input errors should not occur and imply a
corrupted file. But here we think aborting the program with an appropriate
error message usually would be a more appropriate action. Here is
an example showing the use of exceptions in an interactive program, expecting
numbers:
#include <iostream> using namespace::std; int main() { cin.exceptions(ios::failbit); while (true) { try { cout << "enter a number: "; int value; cin >> value; cout << "you entered " << value << endl; } catch (ios::failure const &problem) { cout << problem.what() << endl; cin.clear(); string s; getline(cin, s); } } }
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 exceptionThus, 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 countermeasures are available:
try
block, catching the exception within the
constructor. There may be good reasons for throwing exceptions out of the
constructor, as that is a direct way to inform the code using the constructor
that the object has not become available. But before the exception leaves the
constructor, it should be given a chance to delete memory it already has
allocated. The following skeleton setup of a constructor shows how this can be
realized. Note how any exception that may have been generated is rethrown,
allowing external code to inspect this exception too:
Incomplete::Incomplete() { try { d_memory = new Type; code_maybe_throwing_exceptions(); } catch (...) { delete d_memory; throw; } };
try
block within the constructor's body has no chance to catch
such exceptions. When a class uses pointer data members, and exceptions are
generated after these pointer data members have been initialized, memory
leaks can still be avoided, though. This is accomplished by using smart
pointers, e.g., auto_ptr objects, introduced in section 17.3. As
auto_ptr
objects are objects, their destructors are still called, even
when their the full construction of their composing object fails. In this case
the rule once an object has been constructed its destructor is called when
the object goes out of scope still applies.
Section 17.3.6 covers the use of auto_ptr
objects to prevent
memory leaks when exceptions are thrown out of constructors, even if the
exception is generated by a member initializer.
C++, however, supports an even more generic way to prevent exceptions from leaving functions (or constructors): function try blocks. These function try blocks are discussed in the next section.
The situation we're about to discuss may be compared to a carpenter building a cupboard containing 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 containing 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 its first use.
Weird story? 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 AbortThe final
Abort
indicating that the program has aborted, instead of
displaying a message like Cupboard2 behaves as expected
. Now 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. However, a problem occurs when
Cupboard2
's destructor is called. Of its two composed objects, the
destructor of the second Drawer
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 the exception leaving
Cupboard2
's destructor is thrown, any remaining actions would simply be
ignored, albeit that (as both drawers are properly constructed objects)
left
's destructor would still be called. So this happens here
too. However, left
's destructor also throws an exception. Since we've
already left the context of the second try
block, the programmed
flow control is completely 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. This causes the program to abort.
This situation can be prevented if we ensure that exceptions
never leave destructors. In the cupboard example, Drawer
's destructor
throws an exception leaving the destructor. This should not happen: the
exception should be caught by Drawer
's destructor itself. Exceptions
should never be thrown out of destructors, as we might not be able to catch,
at an outer level, exceptions generated by destructors. As long as we view
destructors as service members performing tasks that are directly related
to the object being destroyed, rather than a member on which we can base any
flow control, this should not be a serious limitation. Here is the skeleton of
a destructor whose code might throw exceptions:
Class::~Class() { try { maybe_throw_exceptions(); } catch (...) {} }
try
block does not
solve the problem (as the exception by then has left the constructor) and is
not a very elegant approach by itself, because of the resulting additional
(and somewhat artificial) nesting level.
Using a nested try
block is illustrated by the next example, where
main()
defines an object of class DataBase
. Assuming that
DataBase
's constructor may throw an exception, there is no way we can
catch the exception in an `outer block' (i.e., in the code calling
main()
), as we don't have an outer block in this situation. Consequently,
we must resort to less elegant solutions like the following:
int main(int argc, char **argv) { try { DataBase db(argc, argv); // may throw exceptions ... // main()'s other code } catch(...) // and/or other handlers { ... } }This approach may potentially produce very complex code. If multiple objects are defined, or if multiple sources of exceptions are identifiable within the
try
block, we either get a complex series of exception
handlers, or we have to use multiple nested try
blocks, each using its own
set of catch
-handlers.
None of these approaches, however, solves the basic problem: how can exceptions generated in a local context be caught before the local context has disappeared?
A function's local context remains accessible when its body is defined as
a
function try block. A function try block consists of a try
block
and its associated handlers, defining the function's body. When a function try
block is used, the function itself may catch any exception its code may
generate, even if these exceptions are generated in member initializer lists
of constructors.
The following example shows how a function try block might have been
deployed in the above main()
function. Note how the try
block and its
handler now replace the plain function body:
int main(int argc, char **argv) try { DataBase db(argc, argv); // may throw exceptions ... // main()'s other code } catch(...) // and/or other handlers { ... }Of course, this still does not enable us have exceptions thrown by
DataBase
's constructor itself caught locally by DataBase
's
constructor. Function try blocks, however, may also be used when implementing
constructors. In that case, exceptions thrown by base class initializers
(cf. chapter 13) or member initializers may also be caught by
the constructor's exception handlers. So let's try to implement this approach.
The following example shows a function try block being used by a
constructor. Note that the grammar requires us to put the try
keyword
even before the
member initializer list's colon:
#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; }In this example, the exception thrown by the
Throw
object is first
caught by the object itself. Then it is rethrown. As the Composer
's
constructor uses a function try block, Throw
's rethrown exception is also
caught by Composer
's exception handler, even though the exception was
generated inside its member initializer list.
However, 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 exceptions that otherwise remain uncaught:
Throw's exception handled locally by Throw() Composer() caught exception as well terminate called after throwing an instance of 'int' AbortThe reason for this is actually stated in the C++ standard: at the end of a catch-handler implemented as part of a destructor's or constructor's function try block, the original exception is automatically rethrown. The exception is not rethrown if the handler itself throws another exception, and it is not retrown by catch-handlers that are part of try blocks of other functions. Only constructors and destructors are affected. Consequently, to repair the above program another, outer, exception handler is still required. A simple repair (applicable to all programs except those having global objects whose constructors or destructors use function try blocks) is to provide
main
with a function try block. In the above example this would
boil down to:
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 constructor or function using a function try block also declares the exception types it may throw, then the function try block must follow the function's exception specification list.
All standard exceptions (and all user-defined classes derived from
the class
std::exception
) offer the member
char const *what() const;describing in a short textual message the nature of the exception.
Four classes derived from std::exception
are offered by the language:
std::bad_alloc
: thrown when
operator new
fails;
std::bad_exception
: thrown when a function tries to generate another
type of exception than declared in its
function throw list;
std::bad_cast
: thrown in the context of polymorphism (see section
14.5.1);
std::bad_typeid
: also thrown in the context of polymorphism (see
section 14.5.2);