Выбрать главу

Throwing an exception

If you encounter an exceptional situation in your code—that is, one in which 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 that contains that information and "throwing" it out of your current context. This is called throwing an exception. Here’s what it looks like:.

//: C01:MyError.cpp

class MyError {

   const char* const data;

public:

   MyError(const char* const msg = 0) : data (msg) {}

};

void f() {

   // Here we "throw" an exception object:

   throw MyError("something bad happened");

}

int main() {

   // As you’ll see shortly,

   // we’ll want a "try block" here:

   f();

} ///:~

MyError is an ordinary class, which in this case takes a char* as a constructor argument. You can use any type when you throw (including built-in types), but usually you’ll create special classes for throwing exceptions.

The keyword throw causes a number of relatively magical things to happen. First, it creates a copy of the object you’re throwing and, in effect, "returns" it from the function containing the throw expression, even though that object type isn’t normally what the function is designed to return. A naive way to think about exception handling is as an alternate return mechanism (although you find you can get into trouble if you take the analogy too far). You can also exit from ordinary scopes by throwing an exception. In any case, a value is returned, and the function or scope exits.

Any similarity to function returns ends there because where you return is some place completely different from where a normal function call returns. (You end up in an appropriate part of the code—called an exception handler—that might be far removed from where the exception was thrown.) In addition, any local objects created by the time the exception occurs are destroyed. This automatic cleanup of local objects is often called "stack unwinding.".

In addition, you can throw as many different types of objects as you want. Typically, you’ll throw a different type for each category of error. The idea is to store the information in the object and in the name of its class so that someone in a calling context can figure out what to do with your exception.

Catching an exception

As mentioned earlier, 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), the function exits 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 block 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 check for errors by carefully examining the return codes from the functions you use, you need 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 and handle exceptions after the try block. Thus, 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 some place. This place is the exception handler, and you need one exception handler 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

} catch(type3 id3)

  // Etc...

} catch(typeN idN)

  // Handle exceptions of typeN

}

// Normal execution resumes here...

The syntax of a catch clause resembles functions that take a single argument. The identifier (id1, id2, and so on) can be used inside the handler, just like a function argument, although you can omit the identifier if it’s not needed in the handler. The exception type usually 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. It then enters that catch clause, and the exception is considered handled. (The search for handlers stops once the catch clause is found.) Only the matching catch clause executes; control then resumes after the last handler associated with that try block.

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

To illustrate using try and catch, the following variation of Nonlocal.cpp replaces the call to setjmp( ) with a try block and replaces the call to longjmp( ) with a throw statement.

//: C01:Nonlocal2.cpp

// Illustrates exceptions

#include <iostream>

using namespace std;

class Rainbow {

public:

  Rainbow() { cout << "Rainbow()" << endl; }

  ~Rainbow() { cout << "~Rainbow()" << endl; }

};

void oz() {

  Rainbow rb;

  for(int i = 0; i < 3; i++)

    cout << "there's no place like home\n";

  throw 47;

}

int main() {

  try {

    cout << "tornado, witch, munchkins...\n";

    oz();

  }

  catch (int) {