Chapter 24. Handling Errors — Exceptions

In This Chapter

  • Introducing an exceptional way of handling program errors

  • Finding what's wrong with good ol' error returns

  • Examining throwing and catching exceptions

  • Packing more heat into that throw

I know that it's hard to accept, but occasionally functions don't work properly — not even mine. The traditional means of reporting failure is to return some indication to the caller. C++ includes a new, improved mechanism for capturing and handling errors called exceptions. An exception is defined as "a case in which a rule or principle does not apply." Exception is also defined as an objection to something. Either definition works: An exception is an unexpected (and presumably objectionable) condition that occurs during the execution of the program.

The exception mechanism is based on the keywords try, catch, and throw (that's right, more variable names that you can't use). In outline, it works like this: A function trys to get through a piece of code. If the code detects a problem, it throws an error indication that the calling function must catch.

The following code snippet demonstrates how that works in 1s and 0s:

//
//  FactorialException - demonstrate exceptions using
//                       a factorial function
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

// factorial - compute factorial
int factorial(int n) throw (string)
{
// you can't handle negative values of n;
    // better check for that condition first
    if (n < 0)
    {
        throw string("Argument for factorial negative");
    }

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

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

        // this will generate an exception
        cout << "Factorial of −1 is "
             << factorial(−1) << endl;

        // control will never get here
        cout << "Factorial of 5 is "
             << factorial(5) << endl;
    }
    // control passes here
    catch(string error)
    {
        cout << "Error occurred: " << error << endl;
    }
    catch(...)
    {
        cout << "Default catch " << endl;
    }

    // wait until user is ready before terminating program
    // to allow the user to see the program results
    system("PAUSE");
    return 0;
}

main() starts out by creating a block outfitted with the try keyword. Within this block, it can proceed the way it would if the block were not present. In this case, main() attempts to calculate the factorial of a negative number. Not to be hoodwinked, the clever factorial() function detects the bogus request and throws an error indication using the throw keyword. Control passes to the catch phrase, which immediately follows the closing brace of the try block. The second call to factorial() is not performed.

The declaration for factorial() also announces to the compiler that it may throw a string object somewhere under the right conditions. This is a relatively new, and so far optional, feature. If absent, the function may throw any object that it wants. If present, the function can throw only one of the types of objects included in the declaration.

Justifying a New Error Mechanism?

What's wrong with error returns like FORTRAN used to make? Factorials cannot be negative, so I could have said something like "Okay, if factorial() detects an error, it returns a negative number. The actual value indicates the source of the problem." What's wrong with that? That's how it's been done for ages.

Unfortunately, several problems arise. First, although it's true that the result of a factorial can't be negative, other functions aren't so lucky. For example, you can't take the log of a negative number either, but the negative return value trick won't work here — logarithms can be either negative or positive.

Second, there's just so much information that you can store in an integer. Maybe you can have −1 for "argument is negative" and −2 for "argument is too large." But, if the argument is too large, you want to know what the argument is, because that information might help you debug the problem. There's no place to store that type of information.

Third, the processing of error returns is optional. Suppose someone writes factorial() so that it dutifully checks the argument and returns a negative number if the argument is out of range. If a function that calls factorial() doesn't check the error return, returning an error value doesn't do any good. Sure, you can make all kinds of menacing threats, such as "You will check your error returns or else," and the programmer may have the best of intentions, but you all know that people get lazy and return to their old, non-error-checking ways.

Even if you do check the error return from factorial() or any other function, what can the function do with the error? It can probably do nothing more than output an error message of your own and return another error indication to the caller, which probably does the same. Pretty soon, there's more error detection code than "real" code.

The exception mechanism addresses these problems by removing the error path from the normal code path. Furthermore, exceptions make error handling obligatory. If your function doesn't handle the thrown exception, control passes up the chain of called functions until C++ finds a function to handle the error. This also gives you the flexibility to ignore errors that you can't do anything about anyway. Only the functions that can actually correct the problem need to catch the exception.

Examining the Exception Mechanism

Take a closer look at the steps that the code goes through to handle an exception. When the throw occurs, C++ first copies the thrown object to some neutral place. It then begins looking for the end of the current try block.

If a try block is not found in the current function, control passes to the calling function. A search is then made of that function. If no try block is found there, control passes to the function that called it, and so on up the stack of calling functions. This process is called 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.

When the encasing try block is found, the code searches the first catch phrase immediately following the closing brace of the catch block. If the object thrown matches the type of argument specified in the catch statement, control passes to that catch phrase. If not, a check is made of the next catch phrase. If no matching catch phrases are found, the code searches for the next higher level try block in an ever-outward spiral until an appropriate catch can be found. If no catch phrase is found, the program is terminated.

Consider the following example:

// CascadingException - the following program demonstrates
//              an example of stack unwinding; it also
//              shows how the throw() clause is used in
//              the function declaration
#include <cstdio>

#include <cstdlib>
#include <iostream>
using namespace std;

// prototypes of some functions that we will need later
void f1() throw();
void f2();
void f3() throw(int);

class Obj
{
  public:
    Obj(char c) : label(c)
    { cout << "Constructing object " << label << endl;}
    ~Obj()
    { cout << "Destructing object " << label << endl; }

  protected:
    char label;
};

int main(int nNumberofArgs, char* pszArgs[])
{
    f1();

    // wait until user is ready before terminating program
    // to allow the user to see the program results
    system("PAUSE");
    return 0;
}

// f1 -an empty throw() clause in the declaration of this
//     function means that it does not throw an exception
void f1() throw()
{
    Obj a('a'),
    try
    {
        Obj b('b'),
        f2();
    }
    catch(float f)
    {
        cout << "Float catch" << endl;
    }
    catch(int i)
    {
        cout << "Int catch" << endl;
    }
    catch(...)
    {
        cout << string("Generic catch") << endl;
    }
}

// f2 - the absence of a throw() clause in the
//      declaration of this function means that it may
//      throw any kind of object
void f2()
{
    try
    {
        Obj c('c'),
        f3();
    }
    catch(string msg)
    {
        cout << "String catch" << endl;
    }
}

// f3 - this function may throw an int object
void f3() throw(int)
{
    Obj d('d'),
    throw 10;
}

The output from executing this program appears as follows:

Constructing object a
Constructing object b
Constructing object c
Constructing object d
Destructing object d
Destructing object c
Destructing object b
Int catch
Destructing object a
Press any key to continue ...

First, you see the four objects a, b, c, and d being constructed as main() calls f1() which calls f2() which calls f3(). Rather than return, however, f3() throws the integer 10. Because no try block is defined in f3(), C++ unwinds f3()'s stack, causing object d to be destructed. The next function up the chain, f2() defines a try block, but its only catch phrase is designed to handle string, which doesn't match the int thrown. Therefore, C++ continues looking. This unwinds f2()'s stack, resulting in object c being destructed.

Back in f1(), C++ finds another try block. Exiting that block causes object b to go out of scope. C++ skips the first catch phrase for a float. The next catch phrase matches the int exactly, so C++ passes control to this phrase.

Control passes from the catch(int) phrase to the closing brace of the final catch phrase and from there back to main(). The final catch(...) phrase, which would catch any object thrown, is skipped because a matching catch phrase was already found.

Tip

Notice that f2() does not declare the types of objects it might throw but the functions f1() and f3() do. Why is this descriptor optional? The throw specification in the declaration was added to the definition of the language long after the exception mechanism itself. First, the new standard wants to remain compatible with the existing C++ code that doesn't declare the types of objects thrown. In addition, it's not clear that forcing the user to declare the type of object thrown is a good idea.

If I wanted to include the type of objects thrown by f2(), I would have to declare it as follows:

void f2() throw(int);

Even though f2() doesn't throw an int directly, it does so indirectly by calling f3() and not catching the int thrown there. In a real-world program, it can get complicated trying to keep track of all the objects that a function could possibly throw.

What Kinds of Things Can I Throw?

The thing following the throw keyword is actually an expression that creates an object of some kind. In the examples so far, I've thrown an int and a string object, but throw can handle any type of object. This means you can throw almost as much information as you want. Consider the following update to the factorial program, CustomExceptionClass:

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

// MyException - generic exception handling class
class MyException
{
  public:
    MyException(const char* pMsg, int n,
                const char* pFunc,
const char* pFile, int nLine)
      : msg(pMsg), errorValue(n),
        funcName(pFunc), file(pFile), lineNum(nLine)
    {}

    virtual string display()
    {
        ostringstream out;
        out << "Error <" << msg << ">"
            << " - value is " << errorValue << "
"
            << "in function " << funcName << "()
"
            << "in file " << file
            << " line #" << lineNum << ends;
        return out.str();
    }
  protected:
    // error message
    string msg;
    int    errorValue;

    // function name, file name and line number
    // where error occurred
    string funcName;
    string file;
    int lineNum;
};

// factorial - compute factorial
int factorial(int n) throw(MyException)
{
    // you can't handle negative values of n;
    // better check for that condition first
    if (n < 0)
    {
        throw MyException("Negative argument not allowed",
                        n, __func__, __FILE__, __LINE__);
    }

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

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

        // this will generate an exception
        cout << "Factorial of −1 is "
             << factorial(−1) << endl;

        // control will never get here
        cout << "Factorial of 5 is "
             << factorial(5) << endl;
    }
    // control passes here
    catch(MyException e)
    {
        cout << e.display() << endl;
    }
    catch(...)
    {
        cout << "Default catch " << endl;
    }

    // wait until user is ready before terminating program
    // to allow the user to see the program results
    system("PAUSE");
    return 0;
}

This program appears much the same as the factorial program at the beginning of this chapter. The difference is the use of a user-defined MyException class that contains more information concerning the nature of the error than a simple string contains. The factorial program is able to throw the error message, the illegal value, and the exact location where the error occurred.

Note

__FILE__, __LINE__ and __func__ are intrinsic #defines that are set to the name of the source file, the current line number in that file, and the name of the current function, respectively.

The catch snags the MyException object and then uses the built-in display() member function to display the error message. (See Chapter 23 for a review of how to use the ostringstream class to format an internal string.) The output from this program appears as follows:

Factorial of 3 is 6
Error <Negative argument not allowed> - value is −1
in function factorial()
in file C:CPP_ProgramsChap25CustomExceptionClassmain.cpp line #53
Press any key to continue ...

Just Passing Through

A function that allocates resources locally may need to catch an exception, do some processing and then rethrow it up the stack chain. Consider the following example:

void fileFunc()
{
    ofstream* pOut = new ofstream("File.txt");
    otherFunction();
    delete pOut;
}

As anyone who's read Chapter 8 knows, the memory allocated by new isn't returned to the heap automatically. If otherFunction() were to throw an exception, control would exit the program without invoking delete, and the memory allocated at the beginning of fileFunc() would be lost.

To avoid this problem, fileFunc() can include a catch(...) to catch any exception thrown:

void fileFunc()
{
    ofstream* pOut = new ofstream("File.txt");
    try
    {
      otherFunction();

      delete pOut;
    }
    catch(...)
    {
        delete pOut;
        throw;
    }
}

Within this phrase, fileFunc() returns the memory it allocated earlier to the heap. However, it is not in a position to process the remainder of the exception because it has no idea what could have gone wrong. It doesn't even know what type of object it just caught.

The throw keyword without any arguments rethrows the current exception object back up the chain to some function that can properly process the error.

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

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