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:
//: C01:Nonlocal.cpp // setjmp() & longjmp() //{L} ../TestSuite/Test #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.[4] 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:
//: C01:Except.cpp // Basic exceptions // Exception specifications & unexpected() //{L} ../TestSuite/Test #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" << endl; exit(0); } 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 (exit(0) is used here so that the
book’s make process is not aborted). 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). On Unix systems, abort( ) also causes a core
dump.
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:
//: C01:Terminator.cpp // Use of set_terminate() // Also shows uncaught exceptions //{L} ../TestSuite/Test #include <exception> #include <iostream> #include <cstdlib> using namespace std; void terminator() { cout << "I'll be back!" << endl; exit(0); } 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.
[ Leave this out of the compile for now
by leaving off the colon after the //]
//: C01:FunctionTryBlock.cpp // Function-level try blocks //{L} ../TestSuite/Test //{-msc} //{-bor} #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 prints a message and
exits the program:
//: C01:Cleanup.cpp // Exceptions clean up objects //{L} ../TestSuite/Test //{-g++3} g++3.0.1 dumps core #include <fstream> #include <exception> #include <cstring> using namespace std; ofstream out("cleanup.out"); class Noisy { static int i; int objnum; enum {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; exit(0); // 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 then exits the program.
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,
//: C01:Rawp.cpp // Naked pointers //{L} ../TestSuite/Test #include <fstream> #include <cstdlib> using namespace std; ofstream out("rawp.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:
//: C01:Wrapped.cpp // Safe, atomic pointers //{L} ../TestSuite/Test #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,
//: C01:Autoexcp.cpp // No matching conversions //{L} ../TestSuite/Test #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.
//: C01:Basexcpt.cpp // Exception hierarchies //{L} ../TestSuite/Test #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 exit
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:
//: C01:Catchref.cpp // Why catch by reference? //{L} ../TestSuite/Test #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.
[4]
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.