Chapter 32

I Take Exception!

In This Chapter

arrow Introducing the exception mechanism for handling program errors

arrow Examining the mechanism in detail

arrow Creating your own custom exception class

I know it’s hard to accept, but occasionally programs don’t work properly — not even mine. The traditional means of reporting a failure within a function is to return some indication to the caller, usually as a return value. Historically, C and C++ programmers have used 0 as the “all clear” indicator — and anything else as meaning an error occurred — the exact value returned indicates the nature of the error.

The problem with this approach is that people generally don’t check all the possible error returns. It’s too much trouble. And if you were to check all the possible error returns, pretty soon you wouldn’t see the “real code” because of all those error paths that are almost never executed.

Finally, you can embed just so much information in a single return value. For example, the factorial() function could return a −1 for “negative argument” (the factorial of a negative number is not defined) and a −2 for “argument too large” (factorials get large very quickly — factorial(100) is well beyond the range of an int). But if the program were to return a −2, wouldn’t you like to know the value of that “too-large argument”? There’s no easy way to embed that information in the return.

The fathers (and mothers) of C++ decided that the language needed a better way of handling errors, so they invented the exception mechanism that has since been duplicated in many similar languages. Exceptions are the subject of this chapter.

The Exception Mechanism

The exception mechanism is a way for functions to report errors so that the error is not ignored even if the calling function does nothing. It’s based on three new keywords: try, catch, and throw (that’s right, more variable names that you can’t use). The exception mechanism works like this: A function tries to make it through a block of code without error. If the program does detect a problem, it throws an error indicator that a calling function can catch for processing.

The following FactorialException demonstrates how this works in ones and zeros:

  // FactorialException - demonstrate the Exception error
//                      handling mechanism with a
//                      factorial function.
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

// factorial - compute factorial
int factorial(int n)
{
    // argument must be positive; throw exception if
    // n is negative
    if (n < 0)
    {
        throw "Argument for factorial is negative";
    }

    // anything over 100 will overflow
    if (n > 100)
    {
        throw "Argument too large";
    }

    // go ahead and calculate factorial
    int nAccum = 1;
    while(n > 1)
    {
        nAccum *= n--;
    }
    return nAccum;
}

int main(int nNumberofArgs, char* pszArgs[])
{
    try
    {
        cout << "Factorial of 3 is "
             << factorial(3)
             << endl;

        cout << "Factorial of -1 is "
             << factorial(-1)
             << endl;

        cout << "Factorial of 5 is "
             << factorial(5)
             << endl;
    }
    catch(const char* pMsg)
    {
        cerr << "Error occurred: " << pMsg << endl;
    }
    catch(...)
    {
        cerr << "Unexpected error thrown" << endl;
    }

    // wait until user is ready before terminating program
    // to allow the user to see the program results
    cout << "Press Enter to continue..." << endl;
    cin.ignore(10, ' '),
    cin.get();    return 0;
}

The main() function starts with the keyword try followed by an open brace and, eventually, a closed brace. Everything within the braces is said to be within a try block. The function then proceeds to display the factorial of three values: 3, −1, and 5. The only problem is that the factorial of a negative number is not defined.

You can see this within the factorial() function. This version of the function now contains a check for a negative argument and for an argument that is so large that it will overflow the int. In the event that either condition is true, control passes to a statement consisting of the keyword throw followed by an ASCIIZ string that contains a description of the error.

Back in main(), at the end of the try block, are two catch phrases. Each consists of the keyword catch followed by an argument. These catch phrases are designed to catch any exceptions thrown from within the try block. The type of the object thrown is compared with the type of the argument in the catch. The control passes to the first one that matches.

The first catch phrase in the FactorialException program catches a pointer to an ASCIIZ string. This catch phrase displays the string. The second catch phrase, the one with the ellipsis for an argument, is designed to catch anything. This wild-card catch phrase also displays a message, but since the catch phrase is so generic, it has no idea from where the exception was thrown or how to interpret the exception, so it just outputs a generic error message.

In practice, the program works like this: The first call to factorial(3) skips over both error conditions and returns the value 6. No problem so far.

The second call, factorial(-1) causes control to pass to the statement throw "Argument for factorial is negative". This command passes control immediately out of factorial() and to the end of the try block where C++ starts comparing the type of "Argument for factorial is negative" (which is const char* by the way — but you knew that) to each of the catch arguments.

Fortunately, the type of object thrown matches the type of the first catch phrase. This displays the string "Error occurred:" followed by the string thrown from within factorial(). Control then passes to the first statement after the last catch phrase, which is the usual call to cout << "Press Enter to continue…".

In execution, the output from the program appears as follows:

  Factorial of 3 is 6
Error occurred: Argument for factorial is negative
Press Enter to continue …

Notice that the call to factorial(5) never gets executed. There is no way to return from a catch block.

Examining the exception mechanism in detail

Now, take a closer look at how C++ processes an exception.

When C++ encounters a throw, it first copies the object thrown to some neutral place other than the local memory of the function. It then starts looking in the current function for the end of the current try block. If it does not encounter one, it then executes a return from the function and continues the search. C++ continues to return and search, return and search until it finds the end of the current try block. This process is known as unwinding the stack.

An important feature of stack unwinding is that as each stack is unwound, objects that go out of scope are destructed just as though the function had executed a return statement. This keeps the program from losing assets or leaving objects dangling. (Unfortunately memory allocated off of the heap is not returned to the heap just because a pointer goes out of scope. Again, this is no different from executing a return.)

When an enclosing try block is found, the code searches the first catch phrase to see if the argument type matches the object thrown. If not, it checks the next catch phrase, and the next if necessary, and so on until a match is found.

If no matching catch phrase is found, then C++ resumes looking for the next higher try block in an ever outward spiral until an appropriate catch can be found. If no matching catch phrase is found, control eventually passes outside of main() where the exception is caught by the C++ library that outputs a generic message and terminates the program.

Once a catch phrase is found, the exception is said to be handled and control passes to the statement following the last catch phrase.

The phrase catch(...) catches all exceptions.

Special considerations for throwing

I need to mention a few special considerations in regard to throwing exceptions. You need to be careful not to throw a pointer to an object in local memory. As the stack is unwound, all local variables are destroyed. C++ will copy the object into a safe memory location to keep it from being destroyed, but there’s no way that C++ can tell what a pointer might be pointing to.

Note that I avoid this problem in the earlier example by throwing a pointer to a const string — these are kept in a different memory area and not on the stack. You can see a better way to avoid this problem in the next section.

Don’t catch an exception if you don’t know what to do with the error. That may sound obvious, but it isn’t really. The exception mechanism allows programmers to handle errors at a level at which they can truly do something about them. For example, if you are writing a data-storage function and you get an exception from a write to the disk, there’s not much point in catching it. The destructor for the output object should close the file, and C++ calls that destructor automagically. Better to let the error propagate up to a level where the program knows what it’s trying to do.

A catch phrase can rethrow an exception by executing the keyword throw; alone (without an argument). This allows the programmer to partially process an error. For example, a database function might catch an exception, close any open tables or databases, and rethrow the exception to the application software to be handled there for good. (Assuming that the destructors haven’t done that stuff already.)

Creating a Custom Exception Class

What follows a throw is actually an expression that creates an object of some kind. In the earlier example, the object is a pointer, but it could be any object you like (with one exception that I mention a little later in this section).

For example, I could create my own class specifically for the purpose of holding information about errors. For the factorial() example, I could create a class ArgOutOfRange that includes everything you need to know about out-of-range arguments. In this way, I could store as much information as needed to debug the error (if it is an error), process the exception, and report the problem accurately to the user.

The following CustomExceptionClass program creates an ArgOutOfRange class and uses it to provide an accurate description of the error encountered in factorial():

  // CustomExceptionClass - demonstrate the flexibility of
//                    the exception mechanism by creating
//                    a custom exception class.
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <sstream>
#include <exception>
using namespace std;

class ArgOutOfRange : public exception
{
  protected:
    string sMsg;
    int nValue;
    int nMaxLegal;
    int nMinLegal;

  public:
    ArgOutOfRange(const char* pszFName, int nVal,
                  int nMin = 0, int nMax = 0)
    {
        nValue = nVal;
        nMinLegal = nMin;
        nMaxLegal = nMax;

        ostringstream out;
        out << "Argument out of range in " << pszFName
            << ", arg is " << nValue;
        if (nMin != nMax)
        {
            out << ", legal range is "
                << nMin << " to " << nMax;
        }
        out << ends;
        sMsg = out.str();
    }

    virtual const char* what()
    {
        return sMsg.c_str();
    }
};


// factorial - compute factorial
int factorial(int n)
{
    // argument must be positive; throw exception if
    // n is negative
    if (n < 0)
    {
        throw ArgOutOfRange("factorial()", n, 0, 100);
    }

    // anything over 100 will overflow
    if (n > 100)
    {
        throw ArgOutOfRange("factorial()", n, 0, 100);
    }

    // go ahead and calculate factorial
    int nAccum = 1;
    while(n > 1)
    {
        nAccum *= n--;
    }
    return nAccum;
}

int main(int nNumberofArgs, char* pszArgs[])
{
    try
    {
        cout << "Factorial of 3 is "
             << factorial(3)
             << endl;

        cout << "Factorial of -1 is "
             << factorial(-1)
             << endl;

        cout << "Factorial of 5 is "
             << factorial(5)
             << endl;
    }
    catch(ArgOutOfRange e)
    {
        cerr << "Error occurred: " << e.what() << endl;
    }
    catch(...)
    {
        cerr << "Unexpected error thrown" << endl;
    }

    // wait until user is ready before terminating program
    // to allow the user to see the program results
    cout << "Press Enter to continue..." << endl;
    cin.ignore(10, ' '),
    cin.get();
    return 0;
}

Here the main() program starts just like the previous example. The factorial() function contains the same tests. Rather than throw a simple character string, however, this version of factorial() throws an object of class ArgOutRange. The constructor for ArgOutOfRange provides room for the name of the function, the value of the offending argument, and the range of legal values for the argument.

All the real work is done in the ArgOutOfRange class. First, this class extends the class exception, which is defined in the exception include file. The exception class defines the virtual member function what() that you must override with a version that outputs your message. Everything else is optional.

tip.eps User-defined exception classes should extend exception so that C++ will know what to do with your exception should you fail to catch it.

The constructor to ArgOutOfRange accepts the name of the function, the value of the argument, and the minimum and maximum legal argument values. Providing a default value for these arguments makes them optional. The constructor uses the ostringstream class (discussed in Chapter 31) to create a complex description of the problem in the internal string object sMsg. It also saves off the arguments themselves.

A complete version of ArgOutOfRange would provide access functions to allow each of these values to be queried from the application code, if desired. I have to leave these details out in order to keep the programs as short as possible.

Back in factorial(), the two throws now throw ArgOutOfRange objects with the appropriate information. The catch back in main() is for an ArgOutOfRange object. This block does nothing more than display an error message along with the description returned by ArgOutRange::what().

Since all the real work was done in the constructor, the what() function doesn’t have to do anything except return a char* pointer to the message stored within the string object.

The output from the program is now very descriptive:

  Factorial of 3 is 6
Error occurred:
Argument out of range in factorial(), arg is -1, legal range is 0 to 100
Press Enter to continue …

Restrictions on exception classes

I’ve mentioned that the exception mechanism can throw almost any type of object. The only real restriction is that the class must be copyable. That means either the default copy constructor provided by C++ is sufficient (that was the case for ArgOutOfRange) or the class provides its own copy constructor.

This restriction is because C++ has to copy the exception object out of local storage and to some “safe place” before unwinding the stack. C++ uses the copy constructor again to copy the object to the catch’s storage area.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.111.179