Catching
an exception
If
a function throws an exception, it must assume that exception is caught and
dealt with. As mentioned before, one of the advantages of C++ exception
handling is that it allows you to concentrate on the problem you’re
actually trying to solve in one place, and then deal with the errors from that
code in another place.
The
try block
If
you’re inside a function and you throw an exception (or a called function
throws an exception), that function will exit in the process of throwing. If
you don’t want a
throw
to
leave a function, you can set up a special block within the function where you
try to solve your actual programming problem (and potentially generate
exceptions). This is called the
try
block
because you try your various function calls there. The try block is an ordinary
scope, preceded by the keyword
try:
try {
// Code that may generate exceptions
}
If
you were carefully checking for errors without using exception handling,
you’d have to surround every function call with setup and test code, even
if you call the same function several times. With exception handling, you put
everything in a try block without error checking. This means your code is a lot
easier to write and easier to read because the goal of the code is not confused
with the error checking.
Exception
handlers
Of
course, the thrown exception must end up someplace. This is the
exception
handler,
and there’s one for every exception type you want to catch. Exception
handlers immediately follow the try block and are denoted by the keyword
catch:
try {
// code that may generate exceptions
} catch(type1 id1) {
// handle exceptions of type1
} catch(type2 id2) {
// handle exceptions of type2
}
// etc...
Each
catch clause (exception handler) is like a little function that takes a single
argument of one particular type. The identifier (
id1,
id2,
and so on) may be used inside the handler, just like a function argument,
although sometimes there is no identifier because it’s not needed in the
handler – the exception type gives you enough information to deal with it.
The
handlers must appear directly after the try block. If an exception is thrown,
the exception-handling mechanism goes hunting for the first handler with an
argument that matches the type of the exception. Then it enters that catch
clause, and the exception is considered handled. (The search for handlers stops
once the catch clause is finished.) Only the matching catch clause executes;
it’s not like a
switch
statement where you need a
break
after each
case
to prevent the remaining ones from executing.
Notice
that, within the try block, a number of different function calls might generate
the same exception, but you only need one handler.
Termination
vs. resumption
There
are two basic models in exception-handling theory. In
termination
(which is what C++ supports) you assume the error is so critical there’s
no way to get back to where the exception occurred. Whoever threw the exception
decided there was no way to salvage the situation, and they don’t
want
to come back.
The
alternative is called
resumption.
It means the exception handler is expected to do something to rectify the
situation, and then the faulting function is retried, presuming success the
second time. If you want resumption, you still hope to continue execution after
the exception is handled, so your exception is more like a function call
– which is how you should set up situations in C++ where you want
resumption-like behavior (that is, don’t throw an exception; call a
function that fixes the problem). Alternatively, place your
try
block inside a
while
loop that keeps reentering the
try
block until the result is satisfactory.
Historically,
programmers using operating systems that supported resumptive exception
handling eventually ended up using termination-like code and skipping
resumption. So although resumption sounds attractive at first, it seems it
isn’t quite so useful in practice. One reason may be the distance that
can occur between the exception and its handler; it’s one thing to
terminate to a handler that’s far away, but to jump to that handler and
then back again may be too conceptually difficult for large systems where the
exception can be generated from many points.
The
exception specification
You’re
not required to inform the person using your function what exceptions you might
throw. However, this is considered very uncivilized because it means he cannot
be sure what code to write to catch all potential exceptions. Of course, if he
has your source code, he can hunt through and look for
throw
statements, but very often a library doesn’t come with sources. C++
provides a syntax to allow you to politely tell the user what exceptions this
function throws, so the user may handle them. This is the
exception
specification
and it’s part of the function declaration, appearing after the argument
list.
The
exception specification reuses the keyword
throw,
followed by a parenthesized list of all the potential exception types. So your
function declaration may look like
void
f() throw(toobig, toosmall, divzero);
With
exceptions, the traditional function declaration
means
that any type of exception may be thrown from the function. If you say
it
means that no exceptions are thrown from a function.
For
good coding policy, good documentation, and ease-of-use for the function
caller, you should always use an exception specification when you write a
function that throws exceptions.
unexpected( )
If
your exception specification claims you’re going to throw a certain set
of exceptions and then you throw something that isn’t in that set,
what’s the penalty? The special function
unexpected( )
is called when you throw something other than what appears in the exception
specification.
set_unexpected( )
unexpected( )
is implemented with a pointer to a function, so you can change its behavior.
You do so with a function called
set_unexpected( )
which, like
set_new_handler( ),
takes the address of a function with no arguments and
void
return value. Also, it returns the previous value of the
unexpected( )
pointer so you can save it and restore it later. To use
set_unexpected( ),
you must include the header file
<exception>.
Here’s an example that shows a simple use of all the features discussed
so far in the chapter:
//: C23:Except.cpp
// Basic exceptions
// Exception specifications & unexpected()
#include <exception>
#include <iostream>
#include <cstdlib>
#include <cstring>
using namespace std;
class Up {};
class Fit {};
void g();
void f(int i) throw (Up, Fit) {
switch(i) {
case 1: throw Up();
case 2: throw Fit();
}
g();
}
// void g() {} // Version 1
void g() { throw 47; } // Version 2
// (Can throw built-in types)
void my_unexpected() {
cout << "unexpected exception thrown";
exit(1);
}
int main() {
set_unexpected(my_unexpected);
// (ignores return value)
for(int i = 1; i <=3; i++)
try {
f(i);
} catch(Up) {
cout << "Up caught" << endl;
} catch(Fit) {
cout << "Fit caught" << endl;
}
} ///:~
The
classes
Up
and
Fit
are created solely to throw as exceptions. Often exception classes will be this
small, but sometimes they contain additional information so that the handlers
can query them.
f( )
is a function that promises in its exception specification to throw only
exceptions of type
Up
and
Fit,
and from looking at the function definition this seems plausible. Version one of
g( ),
called by
f( ),
doesn’t throw any exceptions so this is true. But then someone changes
g( )
so it throws exceptions and the new
g( )
is linked in with
f( ).
Now
f( )
begins to throw a new exception, unbeknown to the creator of
f( ).
Thus the exception specification is violated.
The
my_unexpected( )
function has no arguments or return value, following the proper form for a
custom
unexpected( )
function. It simply prints a message so you can see it has been called, then
exits the program. Your new
unexpected( )
function must not return (that is, you can write the code that way but
it’s an error). However, it can throw another exception (you can even
rethrow the same exception), or call
exit( )
or
abort( ).
If
unexpected( )
throws an exception, the search for the handler starts at the function call
that threw the unexpected exception. (This behavior is unique to
unexpected( ).) Although
the
new_handler( )
function pointer can be null and the system will do something sensible, the
unexpected( )
function pointer should never be null. The default value is
terminate( )
(mentioned later), but whenever you use exceptions and specifications you
should write your own
unexpected( )
to log the error and either rethrow it, throw something new, or terminate the
program.
In
main( ),
the
try
block is within a
for
loop so all the possibilities are exercised. Note that this is a way to achieve
something like resumption –
nest the
try
block inside a
for,
while,
do,
or
if
and cause any exceptions to attempt to repair the problem; then attempt the
try
block again.
Only
the
Up
and
Fit
exceptions are caught because those are the only ones the programmer of
f( )
said would be thrown. Version two of
g( )
causes
my_unexpected( )
to be called because
f( )
then throws an
int.
(You can throw any type, including a built-in type.)
In
the call to
set_unexpected( ),
the return value is ignored, but it can also be saved in a pointer to function
and restored later.
Better
exception specifications?
You
may feel the existing exception specification rules aren’t very safe, and
that
should
mean that no exceptions are thrown from this function. If the programmer wants
to throw any type of exception, you may think she
should
have
to say
void
f() throw(...); // Not in C++
This
would surely be an improvement because function declarations would be more
explicit. Unfortunately you can’t always know by looking at the code in a
function whether an exception will be thrown – it could happen because of
a memory allocation, for example. Worse, existing functions written before
exception handling was introduced may find themselves inadvertently throwing
exceptions because of the functions they call (which may be linked into new,
exception-throwing versions). Thus, the ambiguity, so
means
“Maybe I’ll throw an exception, maybe I won’t.” This
ambiguity is necessary to avoid hindering code evolution.
Catching
any exception
As
mentioned, if your function has no exception specification,
any
type of exception can be thrown. One solution to this problem is to create a
handler that
catches
any type of exception. You do this using the ellipses
in the argument list (á la C):
catch(...) {
cout << "an exception was thrown" << endl;
}
This
will catch any exception, so you’ll want to put it at the
end
of your list of handlers to avoid pre-empting any that follow it.
The
ellipses give you no possibility to have an argument or to know anything about
the type of the exception. It’s a catch-all.
Rethrowing
an exception
Sometimes
you’ll want to rethrow the exception that you just caught, particularly
when you use the ellipses to catch any exception because there’s no
information available about the exception. This is accomplished by saying
throw
with no argument:
catch(...) {
cout << "an exception was thrown" << endl;
throw;
}
Any
further
catch
clauses for the same
try
block are still ignored – the
throw
causes the exception to go to the exception handlers in the next-higher
context. In addition, everything about the exception object is preserved, so
the handler at the higher context that catches the specific exception type is
able to extract all the information from that object.
Uncaught
exceptions
If
none of the exception handlers following a particular
try
block matches an exception, that exception moves to the next-higher context,
that is, the function or
try
block surrounding the
try
block that failed to catch the exception. (The location of this higher-context
try
block is not always obvious at first glance.) This process continues until, at
some level, a handler matches the exception. At that point, the exception is
considered “caught,” and no further searching occurs.
If
no handler at any level catches the exception, it is “uncaught” or
“unhandled.” An uncaught exception also occurs if a new exception
is thrown before an existing exception reaches its handler – the most
common reason for this is that the constructor for the exception object itself
causes a new exception.
terminate( )
If
an exception is uncaught, the special function
terminate( )
is automatically called. Like
unexpected( ),
terminate is actually a pointer to a function. Its default value is the
Standard C library function
abort( ),
which immediately exits the program with no calls to the normal termination
functions (which means that destructors for global and static objects might not
be called).
No
cleanups occur for an uncaught exception; that is, no destructors are called.
If you don’t wrap your code (including, if necessary, all the code in
main( ))
in
a try block followed by handlers and ending with a default handler (
catch(...))
to catch all exceptions, then you will take your lumps. An uncaught exception
should be thought of as a programming error.
set_terminate( )
You
can install your own
terminate( )
function using the standard
set_terminate( )
function,
which returns a pointer to the
terminate( )
function you are replacing, so you can restore it later if you want. Your custom
terminate( )
must take no arguments and have a
void
return value. In addition, any
terminate( )
handler you install must not return or throw an exception, but instead must
call some sort of program-termination function. If
terminate( )
is called, it means the problem is unrecoverable.
Like
unexpected( ),
the
terminate( )
function pointer should never be null.
Here’s
an example showing the use of
set_terminate( ).
Here, the return value is saved and restored so the
terminate( )
function can be used to help isolate the section of code where the uncaught
exception is occurring:
//: C23:Trmnator.cpp
// Use of set_terminate()
// Also shows uncaught exceptions
#include <exception>
#include <iostream>
#include <cstdlib>
using namespace std;
void terminator() {
cout << "I'll be back!" << endl;
abort();
}
void (*old_terminate)()
= set_terminate(terminator);
class Botch {
public:
class Fruit {};
void f() {
cout << "Botch::f()" << endl;
throw Fruit();
}
~Botch() { throw 'c'; }
};
int main() {
try{
Botch b;
b.f();
} catch(...) {
cout << "inside catch(...)" << endl;
}
} ///:~
The
definition of
old_terminate
looks a bit confusing at first: It not only creates a pointer to a function,
but it initializes that pointer to the return value of
set_terminate( ).
Even though you may be familiar with seeing a semicolon right after a
pointer-to-function definition, it’s just another kind of variable and
may be initialized when it is defined.
The
class
Botch
not only throws an exception inside
f( ),
but also in its destructor. This is one of the situations that causes a call to
terminate( ),
as you can see in
main( ).
Even though the exception handler says
catch(...),
which would seem to catch everything and leave no cause for
terminate( )
to be called,
terminate( )
is
called anyway, because in the process of cleaning up the objects on the stack
to handle one exception, the
Botch
destructor is called, and that generates a second exception, forcing a call to
terminate( ).
Thus, a destructor
that throws an exception or causes one to be thrown is a design error.
Function-level
try blocks
//: C23:FunctionTryBlock.cpp
// Function-level try blocks
#include <iostream>
using namespace std;
int main() try {
throw "main";
} catch(const char* msg) {
cout << msg << endl;
} ///:~
Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru