Improved error recovery is
one of the most powerful ways you can increase the robustness of your
code.
show how you can make several function calls with only one
catch, thus greatly reducing the amount of error-handling code you must
write.
Unfortunately, it’s almost accepted practice to ignore
error conditions, as if we’re in a state of denial about errors. Some of
the reason is no doubt the tediousness and code bloat of checking for many
errors. For example, printf( ) returns the
number of characters that were successfully printed, but virtually no one checks
this value. The proliferation of code alone would be disgusting, not to mention
the difficulty it would add in reading the code.
The problem with C’s approach to error handling could be
thought of as one of coupling – the user of a function must tie the
error-handling code so closely to that function that it becomes too ungainly and
awkward to use.
One of the major features in C++ is exception handling,
which is a better way of thinking about and handling errors. With exception
handling,
This chapter examines C’s approach to error
handling (such as it is), why it did not work very well for C, and why it
won’t work at all for C++. Then you’ll learn about try,
throw, and catch, the C++ keywords that support exception
handling.
In most of the examples in this book, assert( )
was used as it was intended: for debugging during development with code that
could be disabled with #define NDEBUG for the shipping product.
Runtime error checking uses the require.h functions developed in Chapter
XX. These were a convenient way to say, “There’s a problem here
you’ll probably want to handle with some more sophisticated code, but you
don’t need to be distracted by it in this example.” The
require.h functions may be enough for small programs, but for complicated
products you may need to write more sophisticated error-handling code.
Error handling is quite straightforward in situations where
you check some condition and you know exactly what to do because you have all
the necessary information in that context. Of course, you just handle the error
at that point. These are ordinary errors and not the subject of this chapter.
The problem occurs when you don’t have enough
information in that context, and you need to pass the error information into a
larger context where that information does exist. There are three typical
approaches in C to handle this situation.
and
perror( ) to support this.) As mentioned
before, the programmer may simply ignore the error information because tedious
and obfuscating error checking must occur with each function call. In addition,
returning from a function that hits an exceptional condition may not make
sense.
)
function (to determine what happens when the event occurs) and
raise( ) (to generate an event). Again, this
has high coupling because it requires the user of any library that generates
signals to understand and install the appropriate signal-handling mechanism;
also in large projects the signal numbers from different libraries may clash
with each other.
o
functions in the Standard C library:
setjmp( ) and
longjmp( ). With setjmp( ) you
save a known good state in the program, and if you get into trouble,
longjmp( ) will restore that state. Again, there is high coupling
between the place where the state is stored and the place where the error
occurs.
When considering error-handling schemes with C++,
there’s an additional very critical problem: The C techniques of signals
and setjmp/longjmp do not call destructors, so objects aren’t properly
cleaned up. This makes it virtually impossible to effectively recover from an
exceptional condition because you’ll always leave objects behind that
haven’t been cleaned up and that can no longer be accessed. The following
example demonstrates this with setjmp/longjmp:
//: C07:Nonlocal.cpp // setjmp() & longjmp() #include <iostream> #include <csetjmp> using namespace std; class Rainbow { public: Rainbow() { cout << "Rainbow()" << endl; } ~Rainbow() { cout << "~Rainbow()" << endl; } }; jmp_buf kansas; void oz() { Rainbow rb; for(int i = 0; i < 3; i++) cout << "there's no place like home\n"; longjmp(kansas, 47); } int main() { if(setjmp(kansas) == 0) { cout << "tornado, witch, munchkins...\n"; oz(); } else { cout << "Auntie Em! " << "I had the strangest dream..." << endl; } } ///:~
setjmp( ) is an odd function because if you call
it directly, it stores all the relevant information about the current processor
state in the jmp_buf and returns zero. In that case it has the behavior
of an ordinary function. However, if you call longjmp( ) using the
same jmp_buf, it’s as if you’re returning from
setjmp( ) again – you pop right out the back end of the
setjmp( ). This time, the value returned is the second argument to
longjmp( ), so you can detect that you’re actually coming back
from a longjmp( ). You can imagine that with many different
jmp_bufs, you could pop around to many different places in the program.
The difference between a local goto (with a label) and this nonlocal goto
is that you can go anywhere with setjmp/longjmp (with some restrictions
not discussed here).
The problem with C++ is that longjmp( )
doesn’t respect objects; in particular it doesn’t call destructors
when it jumps out of a scope.[24] Destructor
calls are essential, so this approach won’t work with
C++.
If you encounter an exceptional situation in your code –
that is, one where you don’t have enough information in the current
context to decide what to do – you can send information about the error
into a larger context by creating an object containing that information and
“throwing” it out of your current context. This is called
throwing an exception. Here’s what it looks like:
throw myerror(“something bad happened”);
myerror
is an ordinary class, which takes a char* as its argument. You can use
any type when you throw (including built-in types), but often you’ll use
special types created just for throwing exceptions.
The keyword throw causes a number of relatively magical
things to happen. First it creates an object that isn’t there under normal
program execution, and of course the constructor is called for that object. Then
the object is, in effect, “returned” from the function, even though
that object type isn’t normally what the function is designed to return. A
simplistic way to think about exception handling is as an alternate return
mechanism, although you get into trouble if you take the analogy too far –
you can also exit from ordinary scopes by throwing an exception. But a value is
returned, and the function or scope exits.
Any similarity to function returns ends there because
where you return to is someplace completely different than for a normal
function call. (You end up in an appropriate exception handler that may be miles
away from where the exception was thrown.) In addition, only objects that were
successfully created at the time of the exception are destroyed (unlike a normal
function return that assumes all the objects in the scope must be destroyed). Of
course, the exception object itself is also properly cleaned up at the
appropriate point.
In addition, you can throw as many different types of objects
as you want. Typically, you’ll throw a different type for each different
type of error. The idea is to store the information in the object and the
type of object, so someone in the bigger context can figure out what to
do with your
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.
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.
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.
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.
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
void f();
means that any type of exception may be
thrown from the function. If you say
void f() throw();
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.
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.
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:
//: C07: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.
You may feel the existing exception specification rules
aren’t very safe, and that
void f();
should mean that no exceptions are thrown from this
function. If the programmer wants to throw any type of exception, you may think
he or 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
void f();
means “Maybe I’ll throw an exception, maybe I
won’t.” This ambiguity is necessary to avoid hindering code
evolution.
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.
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.
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.
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.
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:
//: C07:Terminator.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.
//: C07:FunctionTryBlock.cpp // Function-level try blocks #include <iostream> using namespace std; int main() try { throw "main"; } catch(const char* msg) { cout << msg << endl; } ///:~
Part of the magic of exception handling is that you can pop
from normal program flow into the appropriate exception handler. This
wouldn’t be very useful, however, if things weren’t cleaned up
properly as the exception was thrown. C++ exception handling guarantees that as
you leave a scope, all objects in that scope whose constructors have been
completed will have destructors called.
Here’s an example that demonstrates that
constructors that aren’t
completed don’t have the associated destructors called. It also shows what
happens when an exception is thrown in the middle of the creation of an array of
objects, and an unexpected( ) function that rethrows the unexpected
exception:
//: C07:Cleanup.cpp // Exceptions clean up objects #include <fstream> #include <exception> #include <cstring> using namespace std; ofstream out("cleanup.out"); class Noisy { static int i; int objnum; static const int sz = 40; char name[sz]; public: Noisy(const char* nm="array elem") throw(int){ objnum = i++; memset(name, 0, sz); strncpy(name, nm, sz - 1); out << "constructing Noisy " << objnum << " name [" << name << "]" << endl; if(objnum == 5) throw int(5); // Not in exception specification: if(*nm == 'z') throw char('z'); } ~Noisy() { out << "destructing Noisy " << objnum << " name [" << name << "]" << endl; } void* operator new[](size_t sz) { out << "Noisy::new[]" << endl; return ::new char[sz]; } void operator delete[](void* p) { out << "Noisy::delete[]" << endl; ::delete []p; } }; int Noisy::i = 0; void unexpected_rethrow() { out << "inside unexpected_rethrow()" << endl; throw; // Rethrow same exception } int main() { set_unexpected(unexpected_rethrow); try { Noisy n1("before array"); // Throws exception: Noisy* array = new Noisy[7]; Noisy n2("after array"); } catch(int i) { out << "caught " << i << endl; } out << "testing unexpected:" << endl; try { Noisy n3("before unexpected"); Noisy n4("z"); Noisy n5("after unexpected"); } catch(char c) { out << "caught " << c << endl; } } ///:~
The class Noisy keeps track of objects so you can trace
program progress. It keeps a count of the number of objects created with a
static data member i, and the number of the particular object with
objnum, and a character buffer called name to hold an identifier.
This buffer is first set to zeroes. Then the constructor argument is copied in.
(Note that a default argument string is used to indicate array elements, so this
constructor also acts as a default constructor.) Because the Standard C library
function
strncpy( )stops
copying after a null terminator or the number of characters specified by
its third argument, the number of characters copied in is one minus the size of
the buffer, so the last character is always zero, and a print statement will
never run off the end of the buffer.
There are two cases where a throw can occur in the
constructor. The first case happens if this is the fifth object created (not a
real exception condition, but demonstrates an exception thrown during array
construction). The type thrown is int, which is the type promised in the
exception specification. The second case, also contrived, happens if the first
character of the argument string is ‘z’, in which case a
char is thrown. Because char is not listed in the exception
specification, this will cause a call to unexpected( ).
The array versions of new and delete are
overloaded
for the
class, so you can see when they’re called.
The function unexpected_rethrow( ) prints a
message and rethrows the same exception. It is installed as the
unexpected( ) function in the first line of main( ).
Then some objects of type Noisy are created in a try block, but
the array causes an exception to be thrown, so the object n2 is never
created. You can see the results in the output of the program:
constructing Noisy 0 name [before array] Noisy::new[] constructing Noisy 1 name [array elem] constructing Noisy 2 name [array elem] constructing Noisy 3 name [array elem] constructing Noisy 4 name [array elem] constructing Noisy 5 name [array elem] destructing Noisy 4 name [array elem] destructing Noisy 3 name [array elem] destructing Noisy 2 name [array elem] destructing Noisy 1 name [array elem] Noisy::delete[] destructing Noisy 0 name [before array] caught 5 testing unexpected: constructing Noisy 6 name [before unexpected] constructing Noisy 7 name [z] inside unexpected_rethrow() destructing Noisy 6 name [before unexpected] caught z
Four array elements are successfully created, but in the
middle of the constructor for the fifth one, an exception is thrown. Because the
fifth constructor never completes, only the destructors for objects 1–4
are called.
The storage for the array is allocated separately with a
single call to the global new. Notice that even though delete is
never explicitly called anywhere in the program, the exception-handling system
knows it must call delete to properly release the storage. This behavior
happens only with “normal” versions of operator new. If you
use the placement syntax described
in Chapter XX, the exception-handling mechanism will not call delete for
that object because then it might release memory that was not allocated on the
heap.
Finally, object n1 is destroyed, but not object
n2 because it was never created.
In the section testing unexpected_rethrow( ), the
n3 object is created, and the constructor of n4 is begun. But
before it can complete, an exception is thrown. This exception is of type
char, which violates the exception specification, so the
unexpected( ) function is called (which is
unexpected_rethrow( ), in this case). This rethrows the same
exception, which is expected this time, because
unexpected_rethrow( ) can throw any type of exception. The search
begins right after the constructor for n4, and the char exception
handler catches it (after destroying n3, the only successfully created
object). Thus, the effect of unexpected_rethrow( ) is to take any
unexpected exception and make it expected; used this way it provides a filter to
allow you to track the appearance of unexpected
exceptions and pass them
through.
When writing code with exceptions, it’s particularly
important that you always be asking, “If an exception occurs, will this be
properly cleaned up?” Most of the time you’re fairly safe, but in
constructors there’s a problem: If an exception is thrown before a
constructor is completed, the associated destructor will not be called for that
object. This means you must be especially diligent while writing your
constructor.
The general difficulty is allocating resources in
constructors. If an exception occurs in the constructor, the destructor
doesn’t get a chance to deallocate the resource. This problem occurs most
often with “naked”
pointers. For example,
//: C07:Nudep.cpp // Naked pointers #include <fstream> #include <cstdlib> using namespace std; ofstream out("nudep.out"); class Cat { public: Cat() { out << "Cat()" << endl; } ~Cat() { out << "~Cat()" << endl; } }; class Dog { public: void* operator new(size_t sz) { out << "allocating a Dog" << endl; throw int(47); } void operator delete(void* p) { out << "deallocating a Dog" << endl; ::delete p; } }; class UseResources { Cat* bp; Dog* op; public: UseResources(int count = 1) { out << "UseResources()" << endl; bp = new Cat[count]; op = new Dog; } ~UseResources() { out << "~UseResources()" << endl; delete []bp; // Array delete delete op; } }; int main() { try { UseResources ur(3); } catch(int) { out << "inside handler" << endl; } } ///:~
The output is the following:
UseResources() Cat() Cat() Cat() allocating a Dog inside handler
The UseResources constructor is entered, and the
Cat constructor is successfully completed for the array objects. However,
inside Dog::operator new, an exception is thrown (as an example of an
out-of-memory error). Suddenly, you end up inside the handler, without
the UseResources destructor being called. This is correct because the
UseResources constructor was unable to finish, but it means the
Cat object that was successfully created on the heap is never
destroyed.
To prevent this, guard against these “raw”
resource allocations by placing the allocations inside their own objects with
their own constructors and destructors. This way, each allocation becomes
atomic, as an object, and if it fails, the other resource
allocation objects are properly cleaned up. Templates are an excellent way to
modify the above example:
//: C07:Wrapped.cpp // Safe, atomic pointers #include <fstream> #include <cstdlib> using namespace std; ofstream out("wrapped.out"); // Simplified. Yours may have other arguments. template<class T, int sz = 1> class PWrap { T* ptr; public: class RangeError {}; // Exception class PWrap() { ptr = new T[sz]; out << "PWrap constructor" << endl; } ~PWrap() { delete []ptr; out << "PWrap destructor" << endl; } T& operator[](int i) throw(RangeError) { if(i >= 0 && i < sz) return ptr[i]; throw RangeError(); } }; class Cat { public: Cat() { out << "Cat()" << endl; } ~Cat() { out << "~Cat()" << endl; } void g() {} }; class Dog { public: void* operator new[](size_t sz) { out << "allocating an Dog" << endl; throw int(47); } void operator delete[](void* p) { out << "deallocating an Dog" << endl; ::delete p; } }; class UseResources { PWrap<Cat, 3> Bonk; PWrap<Dog> Og; public: UseResources() : Bonk(), Og() { out << "UseResources()" << endl; } ~UseResources() { out << "~UseResources()" << endl; } void f() { Bonk[1].g(); } }; int main() { try { UseResources ur; } catch(int) { out << "inside handler" << endl; } catch(...) { out << "inside catch(...)" << endl; } } ///:~
The difference is the use of the template to wrap the pointers
and make them into objects. The constructors for these objects are called
before the body of the UseResources constructor, and any of these
constructors that complete before an exception is thrown will have their
associated destructors called.
The PWrap template shows a more typical use of
exceptions than you’ve seen so far: A nested class called
RangeError is created to use in operator[ ]
if its argument is out of range. Because operator[ ] returns a
reference it cannot return zero.
(There are no null references.) This is a true exceptional condition – you
don’t know what to do in the current context, and you can’t return
an improbable value. In this example, RangeError is very simple and
assumes all the necessary information is in the class name, but you may also
want to add a member that contains the value of the index, if that is
useful.
Now the output is
Cat() Cat() Cat() PWrap constructor allocating a Dog ~Cat() ~Cat() ~Cat() PWrap destructor inside handler
Again, the storage allocation for Dog throws an
exception, but this time the array of Cat objects is properly cleaned up,
so there is no memory
leak.
When an exception is thrown, the exception-handling system
looks through the “nearest” handlers in the order they are written.
When it finds a match, the exception is considered handled, and no further
searching occurs.
Matching an exception doesn’t require a perfect match
between the exception and its handler. An object or reference to a derived-class
object will match a handler for the base class. (However, if the handler is for
an object rather than a reference, the exception object is “sliced”
as it is
passed to the handler; this does no damage but loses all the derived-type
information.) If a pointer is thrown, standard pointer conversions are used to
match the exception. However, no automatic type conversions
are
used to convert one exception type to another in the process of matching. For
example,
//: C07:Autoexcp.cpp // No matching conversions #include <iostream> using namespace std; class Except1 {}; class Except2 { public: Except2(Except1&) {} }; void f() { throw Except1(); } int main() { try { f(); } catch (Except2) { cout << "inside catch(Except2)" << endl; } catch (Except1) { cout << "inside catch(Except1)" << endl; } } ///:~
Even though you might think the first handler could be used by
converting an Except1 object into an Except2 using the constructor
conversion, the system will not perform such a conversion during exception
handling, and you’ll end up at the Except1 handler.
//: C07:Basexcpt.cpp // Exception hierarchies #include <iostream> using namespace std; class X { public: class Trouble {}; class Small : public Trouble {}; class Big : public Trouble {}; void f() { throw Big(); } }; int main() { X x; try { x.f(); } catch(X::Trouble) { cout << "caught Trouble" << endl; // Hidden by previous handler: } catch(X::Small) { cout << "caught Small Trouble" << endl; } catch(X::Big) { cout << "caught Big Trouble" << endl; } } ///:~
Here, the exception-handling mechanism will always match a
Trouble object, or anything derived from Trouble, to the
first handler. That means the second and third handlers are never called because
the first one captures them all. It makes more sense to catch the derived types
first and put the base type at the end to catch anything less specific (or a
derived class introduced later in the development cycle).
In addition, if Small and Big represent larger
objects than the base class Trouble (which is often true because you
regularly add data members to derived classes), then those objects are sliced
to fit into
the first handler. Of course, in this example it isn’t important because
there are no additional members in the derived classes and there are no argument
identifiers in the handlers anyway. You’ll usually want to use reference
arguments rather than objects in your handlers to avoid slicing off
information.
The set of
exceptions used with the Standard
C++ library are also available for your own use. Generally it’s easier and
faster to start with a standard exception class than to try to define your own.
If the standard class doesn’t do what you need, you can derive from
it.
The following tables describe the standard
exceptions:
exception |
The base class for all the exceptions thrown by the C++
standard library. You can ask what( ) and get a result that can be
displayed as a character representation. |
logic_error |
Derived from exception. Reports program logic errors,
which could presumably be detected before the program executes. |
runtime_error |
Derived from exception. Reports runtime errors,
which can presumably be detected only when the program executes. |
The iostream exception class ios::failure is also
derived from exception, but it has no further subclasses.
The classes in both of the following tables can be used as
they are, or they can act as base classes to derive your own more specific types
of exceptions.
Exception classes derived from
runtime_error
|
|
---|---|
range_error |
Reports violation of a postcondition. |
overflow_error |
Reports an arithmetic overflow. |
bad_alloc |
Reports a failure to allocate storage. |
For most programmers, especially C programmers, exceptions are
not available in their existing language and take a bit of adjustment. Here are
some guidelines for programming with
exceptions.
Exceptions aren’t the answer to all problems. In fact,
if you simply go looking for something to pound with your new hammer,
you’ll cause trouble. The following sections point out situations where
exceptions are not warranted.
The Standard C signal( )
system, and any similar system, handles asynchronous events: events that
happen outside the scope of the program, and thus events the program cannot
anticipate. C++ exceptions cannot be used to handle asynchronous events because
the exception and its handler are on the same call stack. That is, exceptions
rely on scoping, whereas asynchronous events must be handled by completely
separate code that is not part of the normal program flow (typically, interrupt
service routines or event loops).
This is not to say that asynchronous events cannot be
associated with exceptions. But the interrupt handler should do its job
as quickly as possible and then return. Later, at some well-defined point in the
program, an exception might be thrown based on the interrupt.
If you have enough information to handle an error, it’s
not an exception. You should take care of it in the current context rather than
throwing an exception to a larger context.
Also, C++ exceptions are not thrown for machine-level events
like divide-by-zero. It’s assumed these are dealt with by some other
mechanism, like the operating system or hardware. That way, C++ exceptions can
be reasonably efficient, and their use is isolated to program-level exceptional
conditions.
An exception looks somewhat like an alternate return mechanism
and somewhat like a switch statement, so you can be tempted to use them
for other than their original intent. This is a bad idea, partly because the
exception-handling system is significantly less efficient than normal program
execution; exceptions are a rare event, so the normal program shouldn’t
pay for them. Also, exceptions from anything other than error conditions are
quite confusing to the user of your class or function.
Some programs are quite simple, many utilities, for example.
You may only need to take input and perform some processing. In these programs
you might attempt to allocate memory and fail, or try to open a file and fail,
and so on. It is acceptable in these programs to use
assert( ) or to print a message and
abort( ) the program, allowing the system to
clean up the mess, rather than to work very hard to catch all exceptions and
recover all the resources yourself. Basically, if you don’t need to use
exceptions, you don’t have to.
Another situation that arises is the modification of an
existing program that doesn’t use exceptions. You may introduce a library
that does use exceptions and wonder if you need to modify all your code
throughout the program. Assuming you have an acceptable error-handling scheme
already in place, the most sensible thing to do here is surround the largest
block that uses the new library (this may be all the code in
main( )) with a try block, followed by a
catch(...) and basic error message. You can refine this to whatever
degree necessary by adding more specific handlers, but, in any case, the code
you’re forced to add can be minimal.
You can also isolate your exception-generating code in a try
block and write handlers to convert the exceptions into your existing
error-handling scheme.
It’s truly important to think about exceptions when
you’re creating a library for someone else to use, and you can’t
know how they need to respond to critical error
conditions.
Do use exceptions to
The exception specification is like a function prototype: It
tells the user to write exception-handling code and what exceptions to handle.
It tells the compiler the exceptions that may come out of this
function.
Of course, you can’t always anticipate by looking at the
code what exceptions will arise from a particular function. Sometimes the
functions it calls produce an unexpected exception, and sometimes an old
function that didn’t throw an exception is replaced with a new one that
does, and you’ll get a call to unexpected( ). Anytime you use
exception specifications or call functions that do, you should create your own
unexpected( ) function that logs a message and rethrows the same
exception.
Check out the Standard C++ library exceptions before creating
your own. If a standard exception does what you need, chances are it’s a
lot easier for your user to understand and handle.
If the exception type you want isn’t part of the
standard library, try to derive one from an existing standard exception.
It’s nice for your users if they can always write their code to expect the
what( ) function defined in the exception( ) class
interface.
If you create exceptions for your particular class, it’s
a very good idea to nest the exception classes inside your class to provide a
clear message to the reader that this exception is used only for your class. In
addition, it prevents the pollution of the namespace.
You can nest your exceptions even if you’re deriving
them from C++ standard exceptions.
Exception hierarchies provide a
valuable way to classify the different types of critical errors that may be
encountered with your class or library. This gives helpful information to users,
assists them in organizing their code, and gives them the option of ignoring all
the specific types of exceptions and just catching the base-class type. Also,
any exceptions added later by inheriting from the same base class will not force
all existing code to be rewritten – the base-class handler will catch the
new exception.
Of course, the Standard C++ exceptions are a good example of
an exception hierarchy, and one that you can use to build upon.
You’ll remember from Chapter XX that the only
essential place for MI is if you need to upcast a pointer to your object
into two different base classes – that is, if you need polymorphic
behavior with both of those base classes. It turns out that exception
hierarchies are a useful place for multiple inheritance because a base-class
handler from any of the roots of the multiply inherited
exception class can handle the exception.
If you throw an object of a derived class and it is caught
by value in a handler for an object of the base class, that object is
“sliced” – that is, the derived-class elements are cut off and
you’ll end up with the base-class object being passed. Chances are this is
not what you want because the object will behave like a base-class object and
not the derived class object it really is (or rather, was – before it was
sliced). Here’s an example:
//: C07:Catchref.cpp // Why catch by reference? #include <iostream> using namespace std; class Base { public: virtual void what() { cout << "Base" << endl; } }; class Derived : public Base { public: void what() { cout << "Derived" << endl; } }; void f() { throw Derived(); } int main() { try { f(); } catch(Base b) { b.what(); } try { f(); } catch(Base& b) { b.what(); } } ///:~
The output is
Base Derived
because, when the object is caught by value, it is turned
into a Base object (by the copy-constructor) and must behave that way
in all situations, whereas when it’s caught by reference, only the address
is passed and the object isn’t truncated, so it behaves like what it
really is, a Derived in this case.
Although you can also throw and catch
pointers, by doing so you introduce more coupling –
the thrower and the catcher must agree on how the exception object is allocated
and cleaned up. This is a problem because the exception itself may have occurred
from heap exhaustion. If you throw exception objects, the exception-handling
system takes care of all storage.
Because a
constructor has no return value,
you’ve previously had two choices to report an error during
construction:
This is a
serious problem because C programmers have come to rely on an implied guarantee
that object creation is always successful, which is not unreasonable in C where
types are so primitive. But continuing execution after construction
fails in a C++ program is a guaranteed disaster, so
constructors are one of the most important places to throw exceptions –
now you have a safe, effective way to handle constructor errors. However, you
must also pay attention to pointers inside objects and the way cleanup occurs
when an exception is thrown inside a constructor.
Because destructors
are called in the process of
throwing other exceptions, you’ll never want to throw an exception in a
destructor or cause another exception to be thrown by some action you perform in
the destructor. If this happens, it means that a new exception may be thrown
before the catch-clause for an existing exception is reached, which will
cause a call to terminate( ).
This means that if you call any functions inside a destructor
that may throw exceptions, those calls should be within a try block in
the destructor, and the destructor must handle all exceptions itself. None must
escape from the destructor.
See Wrapped.cpp. A naked pointer usually means
vulnerability in the constructor if resources are allocated for that pointer. A
pointer doesn’t have a destructor, so those resources won’t be
released if an exception is thrown in the
constructor.
Of course it costs something for this new feature; when an
exception is thrown there’s considerable runtime overhead. This is the
reason you never want to use exceptions as part of your normal flow-of-control,
no matter how tempting and clever it may seem. Exceptions should occur only
rarely, so the overhead is piled on the exception and not on the normally
executing code. One of the important design goals for exception handling was
that it could be implemented with no impact on execution speed when it
wasn’t used; that is, as long as you don’t throw an
exception, your code runs as fast as it would without exception handling.
Whether or not this is actually true depends on the particular compiler
implementation you’re using.
Exception handling also causes extra information to be put on
the stack by the compiler, to aid in stack unwinding.
Exception objects are properly passed around like any other
objects, except that they can be passed into and out of what can be thought of
as a special “exception scope” (which may just be the global scope).
That’s how they go from one place to another. When the exception handler
is finished, the exception objects are properly
destroyed.
Error recovery is a fundamental concern for every program you
write, and it’s especially important in C++, where one of the goals is to
create program components for others to use. To create a robust system, each
component must be robust.
The goals for exception handling in C++ are to simplify the
creation of large, reliable programs using less code than currently possible,
with more confidence that your application doesn’t have an unhandled
error. This is accomplished with little or no performance penalty, and with low
impact on existing code.
Basic exceptions are not terribly difficult to learn, and you
should begin using them in your programs as soon as you can. Exceptions are one
of those features that provide immediate and significant benefits to your
project.
[24] You may be
surprised when you run the example – some C++ compilers have extended
longjmp( ) to clean up objects on the stack. This is nonportable
behavior.