MFC Programmer's SourceBook : Thinking in C++
Bruce Eckel's Thinking in C++, 2nd Ed Contents | Prev | Next

Catching an exception

If a function throws an exception, it must assume that exception is caught and dealt with. As mentioned before, one of the advantages of C++ exception handling is that it allows you to concentrate on the problem you’re actually trying to solve in one place, and then deal with the errors from that code in another place.

The try block

If you’re inside a function and you throw an exception (or a called function throws an exception), that function will exit in the process of throwing. If you don’t want a throw to leave a function, you can set up a special block within the function where you try to solve your actual programming problem (and potentially generate exceptions). This is called the try block because you try your various function calls there. The try block is an ordinary scope, preceded by the keyword try:

try {
  // Code that may generate exceptions
}

If you were carefully checking for errors without using exception handling, you’d have to surround every function call with setup and test code, even if you call the same function several times. With exception handling, you put everything in a try block without error checking. This means your code is a lot easier to write and easier to read because the goal of the code is not confused with the error checking.

Exception handlers

Of course, the thrown exception must end up someplace. This is the exception handler, and there’s one for every exception type you want to catch. Exception handlers immediately follow the try block and are denoted by the keyword catch:

try {
// code that may generate exceptions
} catch(type1 id1) {
  // handle exceptions of type1
} catch(type2 id2) {
  // handle exceptions of type2
}
// etc... 

Each catch clause (exception handler) is like a little function that takes a single argument of one particular type. The identifier ( id1, id2, and so on) may be used inside the handler, just like a function argument, although sometimes there is no identifier because it’s not needed in the handler – the exception type gives you enough information to deal with it.

The handlers must appear directly after the try block. If an exception is thrown, the exception-handling mechanism goes hunting for the first handler with an argument that matches the type of the exception. Then it enters that catch clause, and the exception is considered handled. (The search for handlers stops once the catch clause is finished.) Only the matching catch clause executes; it’s not like a switch statement where you need a break after each case to prevent the remaining ones from executing.

Notice that, within the try block, a number of different function calls might generate the same exception, but you only need one handler.

Termination vs. resumption

There are two basic models in exception-handling theory. In termination (which is what C++ supports) you assume the error is so critical there’s no way to get back to where the exception occurred. Whoever threw the exception decided there was no way to salvage the situation, and they don’t want to come back.

The alternative is called resumption. It means the exception handler is expected to do something to rectify the situation, and then the faulting function is retried, presuming success the second time. If you want resumption, you still hope to continue execution after the exception is handled, so your exception is more like a function call – which is how you should set up situations in C++ where you want resumption-like behavior (that is, don’t throw an exception; call a function that fixes the problem). Alternatively, place your try block inside a while loop that keeps reentering the try block until the result is satisfactory.

Historically, programmers using operating systems that supported resumptive exception handling eventually ended up using termination-like code and skipping resumption. So although resumption sounds attractive at first, it seems it isn’t quite so useful in practice. One reason may be the distance that can occur between the exception and its handler; it’s one thing to terminate to a handler that’s far away, but to jump to that handler and then back again may be too conceptually difficult for large systems where the exception can be generated from many points.

The exception specification

You’re not required to inform the person using your function what exceptions you might throw. However, this is considered very uncivilized because it means he cannot be sure what code to write to catch all potential exceptions. Of course, if he has your source code, he can hunt through and look for throw statements, but very often a library doesn’t come with sources. C++ provides a syntax to allow you to politely tell the user what exceptions this function throws, so the user may handle them. This is the exception specification and it’s part of the function declaration, appearing after the argument list.

The exception specification reuses the keyword throw, followed by a parenthesized list of all the potential exception types. So your function declaration may look like

void f() throw(toobig, toosmall, divzero);

With exceptions, the traditional function declaration

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.

unexpected( )

If your exception specification claims you’re going to throw a certain set of exceptions and then you throw something that isn’t in that set, what’s the penalty? The special function unexpected( ) is called when you throw something other than what appears in the exception specification.

set_unexpected( )

unexpected( ) is implemented with a pointer to a function, so you can change its behavior. You do so with a function called set_unexpected( ) which, like set_new_handler( ), takes the address of a function with no arguments and void return value. Also, it returns the previous value of the unexpected( ) pointer so you can save it and restore it later. To use set_unexpected( ), you must include the header file <exception>. Here’s an example that shows a simple use of all the features discussed so far in the chapter:

//: C23:Except.cpp
// Basic exceptions
// Exception specifications & unexpected()
#include <exception>
#include <iostream>
#include <cstdlib>
#include <cstring>
using namespace std;

class Up {};
class Fit {};
void g();

void f(int i) throw (Up, Fit) {
  switch(i) {
    case 1: throw Up();
    case 2: throw Fit();
  }
  g();
}

// void g() {} // Version 1
void g() { throw 47; } // Version 2
// (Can throw built-in types)

void my_unexpected() {
  cout << "unexpected exception thrown";
  exit(1);
}

int main() {
  set_unexpected(my_unexpected);
  // (ignores return value)
  for(int i = 1; i <=3; i++)
    try {
      f(i);
    } catch(Up) {
      cout << "Up caught" << endl;
    } catch(Fit) {
      cout << "Fit caught" << endl;
    }
} ///:~ 

The classes Up and Fit are created solely to throw as exceptions. Often exception classes will be this small, but sometimes they contain additional information so that the handlers can query them.

f( ) is a function that promises in its exception specification to throw only exceptions of type Up and Fit, and from looking at the function definition this seems plausible. Version one of g( ), called by f( ), doesn’t throw any exceptions so this is true. But then someone changes g( ) so it throws exceptions and the new g( ) is linked in with f( ). Now f( ) begins to throw a new exception, unbeknown to the creator of f( ). Thus the exception specification is violated.

The my_unexpected( ) function has no arguments or return value, following the proper form for a custom unexpected( ) function. It simply prints a message so you can see it has been called, then exits the program. Your new unexpected( ) function must not return (that is, you can write the code that way but it’s an error). However, it can throw another exception (you can even rethrow the same exception), or call exit( ) or abort( ). If unexpected( ) throws an exception, the search for the handler starts at the function call that threw the unexpected exception. (This behavior is unique to unexpected( ).)

Although the new_handler( ) function pointer can be null and the system will do something sensible, the unexpected( ) function pointer should never be null. The default value is terminate( ) (mentioned later), but whenever you use exceptions and specifications you should write your own unexpected( ) to log the error and either rethrow it, throw something new, or terminate the program.

In main( ), the try block is within a for loop so all the possibilities are exercised. Note that this is a way to achieve something like resumption – nest the try block inside a for, while, do, or if and cause any exceptions to attempt to repair the problem; then attempt the try block again.

Only the Up and Fit exceptions are caught because those are the only ones the programmer of f( ) said would be thrown. Version two of g( ) causes my_unexpected( ) to be called because f( ) then throws an int. (You can throw any type, including a built-in type.)

In the call to set_unexpected( ), the return value is ignored, but it can also be saved in a pointer to function and restored later.

Better exception specifications?

You may feel the existing exception specification rules aren’t very safe, and that

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 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.

Catching any exception

As mentioned, if your function has no exception specification, any type of exception can be thrown. One solution to this problem is to create a handler that catches any type of exception. You do this using the ellipses in the argument list (á la C):

catch(...) {
  cout << "an exception was thrown" << endl;
}

This will catch any exception, so you’ll want to put it at the end of your list of handlers to avoid pre-empting any that follow it.

The ellipses give you no possibility to have an argument or to know anything about the type of the exception. It’s a catch-all.

Rethrowing an exception

Sometimes you’ll want to rethrow the exception that you just caught, particularly when you use the ellipses to catch any exception because there’s no information available about the exception. This is accomplished by saying throw with no argument:

catch(...) {
  cout << "an exception was thrown" << endl;
  throw;
}

Any further catch clauses for the same try block are still ignored – the throw causes the exception to go to the exception handlers in the next-higher context. In addition, everything about the exception object is preserved, so the handler at the higher context that catches the specific exception type is able to extract all the information from that object.

Uncaught exceptions

If none of the exception handlers following a particular try block matches an exception, that exception moves to the next-higher context, that is, the function or try block surrounding the try block that failed to catch the exception. (The location of this higher-context try block is not always obvious at first glance.) This process continues until, at some level, a handler matches the exception. At that point, the exception is considered “caught,” and no further searching occurs.

If no handler at any level catches the exception, it is “uncaught” or “unhandled.” An uncaught exception also occurs if a new exception is thrown before an existing exception reaches its handler – the most common reason for this is that the constructor for the exception object itself causes a new exception.

terminate( )

If an exception is uncaught, the special function terminate( ) is automatically called. Like unexpected( ), terminate is actually a pointer to a function. Its default value is the Standard C library function abort( ), which immediately exits the program with no calls to the normal termination functions (which means that destructors for global and static objects might not be called).

No cleanups occur for an uncaught exception; that is, no destructors are called. If you don’t wrap your code (including, if necessary, all the code in main( )) in a try block followed by handlers and ending with a default handler ( catch(...)) to catch all exceptions, then you will take your lumps. An uncaught exception should be thought of as a programming error.

set_terminate( )

You can install your own terminate( ) function using the standard set_terminate( ) function, which returns a pointer to the terminate( ) function you are replacing, so you can restore it later if you want. Your custom terminate( ) must take no arguments and have a void return value. In addition, any terminate( ) handler you install must not return or throw an exception, but instead must call some sort of program-termination function. If terminate( ) is called, it means the problem is unrecoverable.

Like unexpected( ), the terminate( ) function pointer should never be null.

Here’s an example showing the use of set_terminate( ). Here, the return value is saved and restored so the terminate( ) function can be used to help isolate the section of code where the uncaught exception is occurring:

//: C23:Trmnator.cpp
// Use of set_terminate()
// Also shows uncaught exceptions
#include <exception>
#include <iostream>
#include <cstdlib>
using namespace std;

void terminator() {
  cout << "I'll be back!" << endl;
  abort();
}

void (*old_terminate)()
  = set_terminate(terminator);

class Botch {
public:
  class Fruit {};
  void f() {
    cout << "Botch::f()" << endl;
    throw Fruit();
  }
  ~Botch() { throw 'c'; }
};

int main() {
  try{
    Botch b;
    b.f();
  } catch(...) {
    cout << "inside catch(...)" << endl;
  }
} ///:~ 

The definition of old_terminate looks a bit confusing at first: It not only creates a pointer to a function, but it initializes that pointer to the return value of set_terminate( ). Even though you may be familiar with seeing a semicolon right after a pointer-to-function definition, it’s just another kind of variable and may be initialized when it is defined.

The class Botch not only throws an exception inside f( ), but also in its destructor. This is one of the situations that causes a call to terminate( ), as you can see in main( ). Even though the exception handler says catch(...), which would seem to catch everything and leave no cause for terminate( ) to be called, terminate( ) is called anyway, because in the process of cleaning up the objects on the stack to handle one exception, the Botch destructor is called, and that generates a second exception, forcing a call to terminate( ). Thus, a destructor that throws an exception or causes one to be thrown is a design error.

Function-level try blocks

//: C23:FunctionTryBlock.cpp
// Function-level try blocks
#include <iostream>
using namespace std;

int main() try {
    throw "main";
} catch(const char* msg) {
  cout << msg << endl;
} ///:~ 

Contents | Prev | Next


Go to CodeGuru.com
Contact: webmaster@codeguru.com
© Copyright 1997-1999 CodeGuru