We're always interested in getting feedback. E-mail us if you like this guide, if you think that important material is omitted, if you encounter errors in the code examples or in the documentation, if you find any typos, or generally just if you feel like e-mailing. Mail to Frank Brokken or use an e-mail form. Please state the concerned document version, found in the title. If you're interested in a printable PostScript copy, pick up your own copy inzip
-format byftp
from ftp.icce.rug.nl/pub/http.
In C there are several ways to have a program 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 proceed at an outer section, skipping
the intermediate levels which would have to be visited if a series of
returns from nested functions would have been used.
In C++ all the above ways to handle flow-breaking situations are still
available. However, the last way, using setjmp()
and longjmp()
isn't
often seen in C++ (or even in C) programs, due to the fact that the
program flow is completely disrupted.
In C++ the alternative to using setjmp()
and longjmp()
are
exceptions. Exceptions are a mechanism by which a controlled non-local
exit is realized within the context of a C++ program, 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 are not disastrous enough for
the program to terminate completely. Also, exceptions provide a flexible layer
of flow control between the short-range return
and the crude exit()
.
In this chapter the use of exceptions and their syntax will be
discussed. First an example of the different impacts exceptions and
setjmp()
and longjmp()
have on the the program will be given. Then
the discussion will dig into the formalities of the use of 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
expressionvalue as an exception. The throw
statement should 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 processed }
Gnu g++
compiler requires a special flag to compile sources in which
exceptions are used. It is quite possible that other compilers require similar
flags, but that hasn't been investigated by us.
If the keywords throw, try
or catch
are used in a sourcetext, or if a
sourcefile contains a function calling another function which may throw an
exception the
-fhandle-exceptions
The easy way-out would of course be to include the -fhandle-exceptions
all
the time, but it appears as though this doesn't always work properly,
sometimes resulting in linker-problems.
Fortunately it is usually well known whether a function may throw exceptions,
either directly or indirectly, and so the need for the
-fhandle-exceptions
Outer
and Inner
. An Outer
object is created in the
main()
function, and the function Outer::fun()
is called.
Then, in the Outer::fun()
function an Inner
object is
allocated. After allocating the Inner
object, its memberfunction 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.h> class Inner { public: Inner(); ~Inner(); void fun(); }; class Outer { public: Outer(); ~Outer(); void fun(); private: }; 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(); }
This program can be compiled and run, producing the following output:
Outer constructor Inner constructor Outer fun Inner fun Inner destructor Outer destructorThis output is completely as expected, and it is 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
13.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 returnvalue 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 returnvalue of the program is zero if only
we would have been 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 returnvalue. 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 returnvalue 1.
Now try to follow these steps by studying the next program source, modified
after the basic program given in section 13.2:
#include <iostream.h> #include <setjmp.h> #include <stdlib.h> 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); }
Running the above program produces the following output:
Outer constructor Inner constructor Outer fun Inner fun() Outer destructorAs will be clear from this output, the destructor of the class
Inner
is
not executed. This is a direct result of the non-local characteristic of the
call to longjmp()
: from the function Inner::fun()
processing continues
immediately in the function setjmp()
in main()
: the call to
Inner::~Inner()
, hiddenly placed at the end of Outer::fun()
is never
executed.
Since the destructors of objects can easily be skipped when longjmp()
and
setjmp()
are used, it's probably best to skip these function completely in
C++ program.
setjmp()
and longjmp()
. In this section an example using exceptions is
presented. Again, the program is derived from the basic program, given in
section 13.2. The syntax of exceptions will be covered
shortly, so please skip over the syntactical peculiarities like throw, try
and catch
. Here comes the sourcetext:
#include <iostream.h> 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 (...) {} }
In this program an exception is thrown, where a longjmp()
was used in
the program in section 13.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, like section 13.2.1, the execution of function Inner::fun()
terminates, albeit with an exception, rather than a longjmp()
. The
exception is caught in main()
, and the program terminates.
Now look at the output generated by this program:
Outer constructor Inner constructor Outer fun Inner fun Inner destructor Outer destructor
Note that the destructor of the Inner
object, created in Outer::fun()
is now called again. On the other hand, execution of the function
Inner::fun()
really terminates at the throw
statement: the insertion
of the text into cout
, just beyond the throw
statement, isn't
performed.
So, with our illustrations we hope to have raised your appetite for
exceptions by showing that
return
-statements, and without having
to terminate the program.
setjmp()
and longjmp()
.
throw
statement. The throw
keyword is
followed by an expression, which results in a value of a certain type. For
example:
throw "Hello world"; // throws a char * throw 18; // throws an int throw new String("hello"); // throws a String *,Although it's possible to throw objects, it is not a good idea to do so. As we have seen in section 13.2.2, objects defined locally in functions are automatically destroyed once exceptions are thrown within these functions. Consequently, a locally defined object that is thrown will also be destroyed, which is unwanted, as the object is supposed to be caught later on.
The next source 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,
and in a real-life situation its destructor would normally have deleted its
name
-field. In the example, however, the memory for the name is static,
and is therefore still available, so we're able to complete out illustration
about what's happening when a locally defined object is thrown.
Let's first take a look at the sourcetext:
#include <iostream.h> class Object { public: Object(char const *name); ~Object(); void fun(); void hello(); private: char const *name; }; Object::Object(char const *n) { name = n; cout << "Object constructor of " << name << "\n"; } Object::~Object() { cout << "Object destructor of " << name << "\n"; } void Object::fun() { Object toThrow("'local object'"); cout << "Object fun() of " << name << "\n"; throw toThrow; } void Object::hello() { cout << "Hello by " << name << "\n"; } int main() { Object out("'main object'"); try { out.fun(); } catch (Object o) { cout << "Caught exception\n"; o.hello(); } }
Now take a close look at the output generated by this program (line numbers
were added by us):
Object constructor of 'main object' (1) Object constructor of 'local object' (2) Object fun() of 'main object' (3) Object destructor of 'local object' (4) Caught exception (5) Hello by 'local object' (6) Object destructor of 'main object' (7)The peculiarity here occurs in line (6): output generated by a local object, just before (in line (4)) destroyed by its destructor....
This is of course not what we want. But apart from that, the program behaves
properly. When the exception is thrown in Object::fun()
, the destructor of
the local object toThrow
is called. Then, the program picks up the
execution again in main()
at the catch
-block. At that point the thrown
toThrow
object is received, and its hello()
function is
called. Fortunately, the Object
destructor didn't destroy the name-field,
otherwise the program would have tried to access foreign memory.
Summarizing, local objects should not be thrown, nor should pointers to local
objects be thrown. However, it is possible to throw pointers or references
to dynamically generated
objects, taking care that the generated object is properly deleted when the
generated exception is caught. So, the Object::fun()
and the
catch
-block can be altered as follows to throw and catch pointers to
Objects
:
void Object::fun() { cout << "Object fun() of " << name << "\n"; throw new Object("'new object'"); } // and in main(): ... catch (Object *o) { cout << "Caught exception\n"; o->hello(); delete o; }Here we see that in
Object::fun()
a new Object
is generated, which is
thereupon deleted when the exception is caught.
Alternatively, realizing that references are little more than pointers,
masquerading as plain variables, references could be used instead of pointers.
Here's the required code:
void Object::fun() { cout << "Object fun() of " << name << "\n"; throw *new Object("'new object'"); } // and in main(): ... catch (Object &o) { cout << "Caught exception\n"; o.hello(); delete &o; }Exceptions are thrown in situations where a function can't continue its normal task anymore, although the program is still able to continue. Imagine a program which is an interactive calculator. The program continuously requests expressions, which are then evaluated. In this case the parsing of the expression may show syntax errors, and the evaluation of the expression may result in expressions which can't be evaluated, e.g., because of the expression resulting in a division by zero. A bit more sophistication would allow the use of variables, and non-existing variables may be referred to.
Each of these situations are enough reason to terminate the processing of the
expression at hand, but there's no need to terminate the program. Each
component of the processing of the expression may therefore throw an
exception. E.g.,
... if (parse(expressionBuffer)) // parsing failed ? throw "Syntax error in expression"; ... if (lookup(variableName)) throw "Variable not defined"; ... if (illegalDivision()) 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
thrown. A function
char const *formatMessage(char const *fmt, ...);
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's processed
there. If it's not possible to process the exception at the middle level,
it's 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 able to process that
particular type of exception.
In our server-client situation a function
initialExceptionHandler(char *exception)
initialExceptionHandler()
shows the
empty throw
statement:
void initialExceptionHandler(char *exception) { if (plainMessage(exception)) handleTheMessage(exception); else throw; }As we will see below (section 13.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? Suppose we have a class Exception
,
containing a memberfunction Exception::Type Exception::severity()
.
This memberfunction 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 memberfunction
toString()
, telling us a bit more about the nature of the generated
exception. This smells a lot like polymorphism, showing process()
as a virtual function for the derived classes Message, Warning, Mistake,
Error
and Fatal
.
Now the program may throw all 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 *e) { // show the plain-text information cout << e->toString() << endl; // Can we process it ? if (e->severity <= Exception::Warning) e->process(); // It's either a message // or a warning else throw; // No, pass it on }Due to polymorphism,
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.
try
-block surrounds statements in which exceptions may be thrown. As
we have seen, the actual throw
statement doesn't have to be placed within
the try
-block, but may be placed in a function which is called from the
try
-block, either directly or indirectly.
The keyword try
is followed by a set of curly braces, which acts like a
standard C++ compound statement: multiple statements and variable
definitions may be placed here.
It is possible (and very common) to create levels in which exceptions may
be thrown. For example, code within the main()
function 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 placed. As
we have seen (in section 13.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 process (uncaught) exceptions is used, which is usually 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 should know what
kind of exceptions it should handle. Therefore, the keyword catch
is
followed by a parameter list having one parameter, which is of the type of the
expression of the thrown exception.
So, an exception handler for char *
exceptions will have the following
form:
catch (char const *message) { // code to handle the message }Earlier (section 13.3) we've seen that such a message doesn't have to be thrown as static string. It's also possible for a function to return a string, which is then thrown as an exception. However, if such a function creates the string to be thrown as an exception dynamically, the exception handler will normally have to delete the allocated memory lest memory leaks away.
Generally close attention must 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 upwards to an outer level exception handler, the received exception
should not be deleted by the inner level handler.
Different exception types 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, the exceptions appearing at the end of a
try
-block may be of different types. In order to catch all the types that
may appear at the end of a try
-block, multiple exception handlers (i.e.,
catch
-blocks) may follow the try
-block.
The order in which the exception handlers are placed is important. When an
exception is thrown, the first exception handler matching the type of the
thrown exception is selected, remaining exception handlers are skipped. So
only one exception handler following a try
-block will be
executed. Consequently, exception handlers should be placed from the ones
having the most specific parameters to the ones having more general
parameters. For example, if exception handlers are defined for
char *
s and void *\
s (i.e., any old pointer) then the exception
handler for the former exception type should be placed before the exception
handler for the latter type:
try { // code may throw char pointers // and other pointers } catch (char *message) { // code processing the char pointers // thrown as exceptions } catch (void *whatever) { // code processing all other pointers // thrown as exceptions }An alternative to construct different types of exception handlers for different types of situations, it is of course also possible to design a specific class whose objects contain information about the reason for the exception. Such an approach was discussed earlier, in section 13.3.1. Using this approach, there's only one handler required, since we know we won't throw other types of exceptions:
try { // code may throw only // Exception pointers } catch (Exception *e) { // code processing the Exception pointer delete e; }The use of the
delete e
statement in the above code indicates that the
Exception
object which could be thrown as an exception in the
try
-block was created dynamically.
When the code of an exception handler that is placed beyond a try
-block
has been processed, the execution of the program continues beyond the last
exception handler following that try
-block (unless the handler uses
return, throw
or exit()
to leave the function prematurely). So we have
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 that the code of that
exception handler is exectuted. 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.
throw
-statement will result in skipping all remaining
statements of the try
-block in which the exception was thrown. 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 construction of the Exception
object may be performed in
various degrees of sophistication. Possibilities are using a plain
new
operator, using static memberfunctions of the class Exception
dedicated to a particular kind of exception, returning a pointer to an
Exception
object, or using objects of classes derived from the class
Exception
, possibly involving polymorphism.
This situation is implemented using the default exception handler, which will
(because of the reason given in the previous section 13.5) be
placed beyond all other, more specific exception handlers. Often the default
exception handler will be used in combination with the empty throw statement,
discused in section 13.3.1.
Here is an example showing the use of a default exception handler:
try { // this code may throw // different types of // exceptions } catch (char *message) { // code to process // char pointers } catch (int value) { // code to process // ints } catch (...) { // code to process other exceptions, // often passing the exception on // to outer level exception handlers: throw; }The reason for passing unspecified exceptions on to outer level exception handlers is simply the fact that they are unspecified: how would you process an exception if you don't know its type? In these situations the outer level exception handlers should of course know what exceptions other than
char *
s and int
s to expect....
These external function may of course throw exceptions. The declaration of
such functions may contain a function throw list, in which the types of
the exceptions that can be thrown by the function are specified. For example,
a function that may throw char *
and int
exceptions can be declared as
void exceptionThrower() throw(char *, int);
A function for which a function throw list was specified is not allowed to
throw other types of exceptions. A run-time error occurs if it does throw
other types of exceptions than mentioned in the function throw list.
If a function throw list is specified in the declaration, it must also be
given in the definition of the function. For example, using declaration
and definition in the same example:
#include <iostream.h> void intThrower() throw(int); void charP_IntThrower() throw (char *, int); void intThrower(int x) throw (int) { if (x) throw x; } void charP_IntThrower() throw (char *, int) { int x; cout << "Enter an int: "; cout.flush(); cin >> x; intThrower(x); throw "from charP_IntThrower() with love"; } int main() { try { charP_IntThrower(); } catch (char *message) { cout << "Text exception: " << message << endl; } catch (int value) { cout << "Int exception: " << value << endl; } return (0); }In the function
charP_IntThrower()
the throw
statement clearly throws
a char *
. However, since IntThrower()
may throw an int
exception,
the function throw list of charP_IntThrower()
must also contain
int
. Try this: remove the int
from the (two!) function throw lists,
compile and link the program and see what happens if you enter the value 5.
If a function doesn't throw exceptions an empty function throw list may be
used. E.g.,
void noExceptions() throw ();
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 all responsibilities of providing the correct handlers is in the
hands of the designer of the program....