Day 18: Advanced Exceptions and Error Handling

The code you have seen in this book has been created for illustration purposes. It has not dealt with errors so that you would not be distracted from the central issues being presented. Real-world programs must take into consideration error conditions, and professional programs must be bulletproof.

Today you will learn

Reviewing Exceptions

Programmers use powerful compilers and sprinkle their code with asserts to catch programming errors. They use design reviews and exhaustive testing to find logic errors.

Exceptions are different, however. You cannot eliminate exceptional circumstances; you only can prepare for them. Your users will run out of memory from time to time, and the only question is what your program will do. The choices are limited to the following:

  1. Crash or hang.

  2. Inform the user and exit gracefully.

  3. Inform the user and allow the user to try to recover and continue.

  4. Take corrective action and continue without disturbing the user.

Although it is not necessary or even desirable for every program you write to automatically and silently recover from all exceptional circumstances, it is clear that you must do better than crashing.

C++ exception handling provides a type-safe, integrated method for coping with the predictable but unusual conditions that arise while running a program.

In C++, an exception is an object that is passed from the area of code where a problem occurs to the part of the code that is going to handle the problem. The type of the exception determines which area of code will handle the problem, and the contents of the object thrown, if any, may be used to provide feedback to the user.

The basic idea behind exceptions is fairly straightforward:

Seeing How Exceptions Are Used

Try blocks are created to surround areas of code that may have a problem. For example:

    try
    {
         SomeDangerousFunction();
    }

Catch blocks handle the exceptions thrown in the try block. For example:

    try
    {
         SomeDangerousFunction();
    }
    catch(OutOfMemory)
    {
         // take some actions
    }
    catch(FileNotFound)
    {
         // take other action
    }

The basic steps in using exceptions follow:

  1. Identify those areas of the program where you begin an operation that might raise an exception, and put them in try blocks.

  2. Create catch blocks to catch the exceptions if they are thrown, and to clean up allocated memory and inform the user as appropriate. Listing 18.1 illustrates the use of try blocks and catch blocks.

New Term: Exceptions are objects used to transmit information about a problem.

New Term: A try block is a block surrounded by braces in which an exception may be thrown.

New Term:A catch block is the block immediately following a try block, in which exceptions are handled.

New Term: When an exception is thrown (or raised), control transfers to the catch block immediately following the current try block.

Note: Some older compilers do not support exceptions. Exceptions are part of the emerging C++ standard, however. All major compiler vendors have committed to supporting exceptions in their next release, if they have not done so already. If you have an older compiler, you will not be able to compile and run the exercises in this chapter. It's still a good idea to read through the entire chapter, however, and return to this material when you upgrade your compiler.

Remember that the listings included in this chapter are for illustrative purposes only. Your compiler might issue a warning if you use the code exactly as written.

Listing 18.1 Raising an Exception

    1:     #include <iostream.h>
    2:
    3:     const int DefaultSize = 10;
    4:
    5:     class Array
    6:     {
    7:     public:
    8:        // constructors
    9:        Array(int itsSize = DefaultSize);
    10:       Array(const Array &rhs);
    11:       ~Array() { delete [] pType;}
    12:
    13:       // operators
    14:       Array& operator=(const Array&);
    15:       int& operator[](int offSet);
    16:       const int& operator[](int offSet) const;
    17:
    18:       // accessors
    19:       int GetitsSize() const { return itsSize; }
    20:
    21:       // friend function
    22:      friend ostream& operator<< (ostream&, const Array&);
    23:
    24:       class xBoundary {};  // define the exception class
    25:    private:
    26:       int *pType;
    27:       int  itsSize;
    28:    };
    29:
    30:
    31:    Array::Array(int size):
    32:    itsSize(size)
    33:    {
    34:       pType = new int[size];
    35:       for (int i = 0; i<size; i++)
    36:         pType[i] = 0;
    37:    }
    38:
    39:
    40:    Array& Array::operator=(const Array &rhs)
    41:    {
    42:       if (this == &rhs)
    43:          return *this;
    44:       delete [] pType;
    45:       itsSize = rhs.GetitsSize();
    46:       pType = new int[itsSize];
    47:       for (int i = 0; i<itsSize; i++)
    48:          pType[i] = rhs[i];
    49:    }
    50:
    51:    Array::Array(const Array &rhs)
    52:    {
    53:       itsSize = rhs.GetitsSize();
    54:       pType = new int[itsSize];
    55:       for (int i = 0; i<itsSize; i++)
    56:          pType[i] = rhs[i];
    57:    }
    58:
    59:
    60:    int& Array::operator[](int offSet)
    61:    {
    62:       int size = GetitsSize();
    63:       if (offSet >= 0 && offSet < GetitsSize())
    64:          return pType[offSet];
    65:       throw xBoundary();
    66:    }
    67:
    68:
    69:    const int& Array::operator[](int offSet) const
    70:    {
    71:       int mysize = GetitsSize();
    72:       if (offSet >= 0 && offSet < GetitsSize())
    73:          return pType[offSet];
    74:       throw xBoundary();
    75:    }
    76:
    77:    ostream& operator<< (ostream& output, const Array& theArray)
    78:    {
    79:       for (int i = 0; i<theArray.GetitsSize(); i++)
    80:          output << "[" << i << "] " << theArray[i] << endl;
    81:       return output;
    82:    }
    83:
    84:    void main()
    85:    {
    86:       Array intArray(20);
    87:       try
    88:       {
    89:          for (int j = 0; j< 100; j++)
    90:          {
    91:             intArray[j] = j;
    92:             cout << "intArray[" << j << "] okay..." << endl;
    93:          }
    94:       }
    95:       catch (Array::xBoundary)
    96:       {
    97:          cout << "Unable to process your input!\n";
    98:       }
    99:       cout << "Done.\n";
    99:    }
Output:
    intArray[0] okay...
    intArray[1] okay...
    intArray[2] okay...
    intArray[3] okay...
    intArray[4] okay...
    intArray[5] okay...
    intArray[6] okay...
    intArray[7] okay...
    intArray[8] okay...
    intArray[9] okay...
    intArray[10] okay...
    intArray[11] okay...
    intArray[12] okay...
    intArray[13] okay...
    intArray[14] okay...
    intArray[15] okay...
    intArray[16] okay...
    intArray[17] okay...
    intArray[18] okay...
    intArray[19] okay...
    Unable to process your input!
    Done.
Analysis:

Listing 18.1 presents a somewhat stripped-down Array class, based on the template developed on day 19. In line 24, a new class is contained within the declaration of the array: Boundary.

This new class is not in any way distinguished as an exception class. xBoundary is just a class like any other. This particular class is incredibly simple: it has no data and no methods. Nonetheless, it is a valid class in every way.

In fact, it is incorrect to say that xBoundary has no methods, because the compiler automatically assigns it a default constructor, destructor, copy constructor, and copy operator (operator equals). xBoundary therefore actually has four class functions, but no data.

Note that declaring Boundary from within Array serves only to couple the two classes together. As discussed on day 15, Array has no special access to xBoundary, nor does xBoundary have preferential access to the members of Array.

In lines 60 through 66 and lines 69 through 75, the offset operators are modified to examine the offset requested and, if it is out of range, to throw the xBoundary class as an exception. The parentheses are required to distinguish between this call to the xBoundary constructor and the use of an enumerated constant.

In line 87, the keyword try begins a try block that ends in line 94. Within that try block, 100 integers are added to the array that was declared in line 86.

In line 95, the catch block to catch xBoundary exceptions is declared.

In the driver program in lines 84 through 89 a try block is created in which each member of the array is initialized. When j (line 89) is incremented to 20, the member at offset 20 is accessed. This causes the test in line 63 to fail, and operator[] raises an xBoundary exception in line 65.

Program control switches to the catch block in line 95, and the exception is caught or handled by the case on the same line, which prints an error message. Program flow drops through to the end of the catch block in line 98.

Try Blocks

A try block is a set of statements that begin with the word try, are followed by an opening brace, and end with a closing brace.

Example:

    try
    {
         Function();
    };

Catch Blocks

A catch block is a series of statements, each of which begins with the word catch, followed by an exception type in parentheses, followed by an opening brace, and ending with a closing brace.

Example:

    Try
    {
         Function();
    };
    Catch (OutOfMemory)
    {
         // take action
    }

Using Try Blocks and Catch Blocks

Figuring out where to put your try blocks is non-trivial; it is not always obvious which actions might raise an exception. The next question is where to catch the exception. You might want to throw all memory exceptions where the memory is allocated, but catch the exceptions high in the program where you deal with the user interface.

When determining try block locations, look to where you allocate memory or use resources. Other things to look for are out-of-bounds errors, illegal input, and so on.

Catching Exceptions

When an exception is thrown, the call stack is examined. The call stack is the list of function calls created when one part of the program invokes another function.

The call stack tracks the execution path. If main() calls the function Animal::GetFavoriteFood(), and GetFavoriteFood() calls Animal::LookupPreferences(), which in turn calls fstream::operator>>(), all these are on the call stack. A recursive function might be on the call stack many times.

The exception is passed up the call stack to each enclosing block. As the stack is "unwound," the destructors for local objects on the stack are invoked, and the objects are destroyed.

One or more catch statements are after each try block. If the exception matches one of the catch statements, it is considered to be handled by having that statement execute. If it doesn't match any catch statements, the unwinding of the stack continues.

If the exception reaches all the way to the beginning of the program (main()) and still is not caught, a built-in handler is called that terminates the program.

It is important to note that the exception unwinding of the stack is a one-way street. As it progresses, the stack is unwound and objects on the stack are destroyed. There is no going back; after the exception is handled, the program continues after the try block of the catch statement that handled the exception.

In listing 18.1, therefore, execution continues in line 99, the first line after the try block of the catch statement that handled the xBoundary exception. Remember that when an exception is raised, program flow continues after the catch block, and not after the point where the exception was thrown. In this case, because there is nothing after the catch block, the function returns.

Using More Than One Catch Specification

It is possible for more than one condition to cause an exception. In this case, the catch statements can be lined up one after another, much as the conditions in a switch statement. The equivalent to the default statement is the "catch everything" statement indicated by catch(...). Listing 18.2 illustrates multiple exception conditions.

Listing 18.2 Multiple Exceptions

    1:     #include <iostream.h>
    2:
    3:     const int DefaultSize = 10;
    4:
    5:     class Array
    6:     {
    7:     public:
    8:        // constructors
    9:        Array(int itsSize = DefaultSize);
    10:       Array(const Array &rhs);
    11:       ~Array() { delete [] pType;}
    12:
    13:       // operators
    14:       Array& operator=(const Array&);
    15:       int& operator[](int offSet);
    16:       const int& operator[](int offSet) const;
    17:
    18:       // accessors
    19:       int GetitsSize() const { return itsSize; }
    20:
    21:       // friend function
    22:      friend ostream& operator<< (ostream&, const Array&);
    23:
    24:     // define the exception classes
    25:       class xBoundary {};
    26:       class xTooBig {};
    27:       class xTooSmall{};
    28:       class xZero {};
    29:       class xNegative {};
    30:    private:
    31:       int *pType;
    32:       int  itsSize;
    33:    };
    34:
    35:
    36:    Array::Array(int size):
    37:    itsSize(size)
    38:    {
    39:       if (size == 0)
    40:          throw xZero();
    41:       if (size < 10)
    42:          throw xTooSmall();
    43:       if (size > 30000)
    44:          throw xTooBig();
    45:       if (size < 1)
    46:          throw xNegative();
    47:
    48:       pType = new int[size];
    49:       for (int i = 0; i<size; i++)
    50:         pType[i] = 0;
    51:    }
    52:
    53:
    54:
    55:    void main()
    56:    {
    57:
    58:       try
    59:       {
    60:          Array intArray(0);
    61:          for (int j = 0; j< 100; j++)
    62:          {
    63:             intArray[j] = j;
    64:             cout << "intArray[" << j << "] okay..." << endl;
    65:          }
    66:       }
    67:       catch (Array::xBoundary)
    68:       {
    69:          cout << "Unable to process your input!\n";
    70:       }
    71:       catch (Array::xTooBig)
    72:       {
    73:          cout << "This array is too big..." << endl;
    74:       }
    75:       catch (Array::xTooSmall)
    76:       {
    77:          cout << "This array is too small..." << endl;
    78:       }
    79:       catch (Array::xZero)
    80:       {
    81:          cout << "You asked for an array of zero objects!" << endl;
    82:       }
    83:       catch (...)
    84:       {
    85:          cout << "Something went wrong, but I've no idea what!" << endl;
    86:       }
    87:       cout << "Done.\n";
    88:    }
Output:
    You asked for an array of zero objects!
    Done.
Analysis:

The implementations of all of Array's methods, except for its constructor, have been left out because they are unchanged from listing 18.1.

Four new classes are created in lines 26 through 29 of listing 18.2: xTooBig, xTooSmall, xZero, and xNegative. In the constructor, in lines 36 through 51, the size passed to the constructor is examined. If it is too big, too small, negative, or zero, an exception is thrown.

The try block is changed to include catch statements for each condition other than negative, which is caught by the "catch everything" statement catch(...) shown in line 83.

Try this with a number of values for the size of the array. Then try putting in -5. You might have expected xNegative to be called, but the order of the tests in the constructor prevented this: size < 10 was evaluated before size < 1. To fix this, swap lines 41 and 42 with lines 45 and 46 and recompile.

Using Exception Hierarchies

Exceptions are classes and, as such, they can be derived from. It may be advantageous to create a class xSize, and to derive from it xZero, xTooSmall, xTooBig, and xNegative. Some functions therefore might just catch xSize errors, while other functions might catch the specific type of xSize error. Listing 18.3 illustrates this idea.

Listing 18.3 Class Hierarchies and Exceptions

    1:     #include <iostream.h>
    2:
    3:     const int DefaultSize = 10;
    4:
    5:     class Array
    6:     {
    7:     public:
    8:        // constructors
    9:        Array(int itsSize = DefaultSize);
    10:       Array(const Array &rhs);
    11:       ~Array() { delete [] pType;}
    12:
    13:       // operators
    14:       Array& operator=(const Array&);
    15:       int& operator[](int offSet);
    16:       const int& operator[](int offSet) const;
    17:
    18:       // accessors
    19:       int GetitsSize() const { return itsSize; }
    20:
    21:       // friend function
    22:      friend ostream& operator<< (ostream&, const Array&);
    23:
    24:     // define the exception classes
    25:       class xBoundary {};
    26:       class xSize {};
    27:       class xTooBig : public xSize {};
    28:       class xTooSmall : public xSize {};
    29:       class xZero  : public xTooSmall {};
    30:       class xNegative  : public xSize {};
    31:    private:
    32:       int *pType;
    33:       int  itsSize;
    34:    };
    35:
    36:
    37:    Array::Array(int size):
    38:    itsSize(size)
    39:    {
    40:       if (size == 0)
    41:          throw xZero();
    42:       if (size > 30000)
    43:          throw xTooBig();
    44:       if (size <1)
    45:          throw xNegative();
    46:       if (size < 10)
    47:          throw xTooSmall();
    48:
    49:       pType = new int[size];
    50:       for (int i = 0; i<size; i++)
    51:         pType[i] = 0;
    52:    }
Output:
    This array is too small...
    Done.
Analysis:

Listing 18.3 leaves out the implementation of the array functions because they are unchanged, and it leaves out main() because it is identical to that in listing 18.2.

The significant change is in lines 26 through 30, where the class hierarchy is established. Classes xTooBig, xTooSmall, and xNegative are derived from xSize; and xZero is derived from xTooSmall.

The Array is created with size zero, but what's this? The wrong exception appears to be caught! Examine the catch block carefully, however, and you will find that it looks for an exception of type xTooSmall before it looks for an exception of type xZero. Because an xZero object is thrown and an xZero object is an xTooSmall object, it is caught by the handler for xTooSmall. Once handled, the exception is not passed on to the other handlers, so the handler for xZero never is called.

The solution to this problem is to carefully order the handlers so that the most specific handlers come first and the less specific handlers come later. In this particular example, switching the placement of the two handlers xZero and xTooSmall will fix the problem.

Exception Objects

Often, you will want to know more than just what type of exception was thrown so that you can respond properly to the error. Exception classes are like any other class. You are free to provide data, initialize that data in the constructor, and read that data at any time. Listing 18.4 illustrates how to do this.

Listing 18.4 Getting Data Out of an Exception Object

    1:     #include <iostream.h>
    2:
    3:     const int DefaultSize = 10;
    4:
    5:     class Array
    6:     {
    7:     public:
    8:        // constructors
    9:        Array(int itsSize = DefaultSize);
    10:       Array(const Array &rhs);
    11:       ~Array() { delete [] pType;}
    12:
    13:       // operators
    14:       Array& operator=(const Array&);
    15:       int& operator[](int offSet);
    16:       const int& operator[](int offSet) const;
    17:
    18:       // accessors
    19:       int GetitsSize() const { return itsSize; }
    20:
    21:       // friend function
    22:      friend ostream& operator<< (ostream&, const Array&);
    23:
    24:     // define the exception classes
    25:       class xBoundary {};
    26:       class xSize
    27:       {
    28:       public:
    29:          xSize(int size):itsSize(size) {}
    30:          ~xSize(){}
    31:          int GetSize() { return itsSize; }
    32:       private:
    33:          int itsSize;
    34:       };
    35:
    36:       class xTooBig : public xSize
    37:       {
    38:       public:
    39:          xTooBig(int size):xSize(size){}
    40:       };
    41:
    42:       class xTooSmall : public xSize
    43:       {
    44:       public:
    45:          xTooSmall(int size):xSize(size){}
    46:       };
    47:
    48:       class xZero  : public xTooSmall
    49:       {
    50:       public:
    51:          xZero(int size):xTooSmall(size){}
    52:       };
    53:
    54:       class xNegative : public xSize
    55:       {
    56:       public:
    57:          xNegative(int size):xSize(size){}
    58:       };
    59:
    60:    private:
    61:       int *pType;
    62:       int  itsSize;
    63:    };
    64:
    65:
    66:    Array::Array(int size):
    67:    itsSize(size)
    68:    {
    69:       if (size == 0)
    70:          throw xZero(size);
    71:       if (size > 30000)
    72:          throw xTooBig(size);
    73:       if (size <1)
    74:          throw xNegative(size);
    75:       if (size < 10)
    76:          throw xTooSmall(size);
    77:
    78:       pType = new int[size];
    79:       for (int i = 0; i<size; i++)
    80:         pType[i] = 0;
    81:    }
    82:
    83:
    84:    void main()
    85:    {
    86:
    87:       try
    88:       {
    89:          Array intArray(9);
    90:          for (int j = 0; j< 100; j++)
    91:          {
    92:             intArray[j] = j;
    93:             cout << "intArray[" << j << "] okay..." << endl;
    94:          }
    95:       }
    96:       catch (Array::xBoundary)
    97:       {
    98:          cout << "Unable to process your input!\n";
    99:       }
    100:      catch (Array::xZero theException)
    101:      {
    102:         cout << "You asked for an array of zero objects!" << endl;
    103:         cout << "Received " << theException.GetSize() << endl;
    104:      }
    105:      catch (Array::xTooBig theException)
    106:      {
    107:         cout << "This array is too big..." << endl;
    108:         cout << "Received " << theException.GetSize() << endl;
    109:      }
    110:      catch (Array::xTooSmall theException)
    111:      {
    112:         cout << "This array is too small..." << endl;
    113:         cout << "Received " << theException.GetSize() << endl;
    114:      }
    115:      catch (...)
    116:      {
    117:         cout << "Something went wrong, but I've no idea what!" << endl;
    118:      }
    119:      cout << "Done.\n";
    120:   }
Output:
    This array is too small...
    Received 9
    Done.
Analysis:

The declaration of xSize has been modified to include a member variable, itsSize, in line 33 and a member function, GetSize(), in line 31. Additionally, a constructor has been added that takes an integer and initializes the member variable, as shown in line 29.

The derived classes declare a constructor that does nothing but initialize the base class. No other functions were declared, partly to save space in the listing.

The catch statements in lines 100 through 118 are modified to name the exception they catch, theException, and to use this object to access the data stored in itsSize.

Note: Keep in mind that if you are constructing an exception, it is because an exception has been raised; something has gone wrong and your exception should be careful not to kick off the same problem. If you are creating an OutOfMemory exception, therefore, you probably don't want to allocate memory in its constructor.

It is a tedious and error-prone process to have each of these catch statements individually print the appropriate message. This job belongs to the object, which knows what type of object it is and what value it received. Listing 18.5 takes a more object-oriented approach to this problem, using virtual functions so that each exception does the right thing.

Listing 18.5 Passing by Reference and Using Virtual Functions in Exceptions

    1:     #include <iostream.h>
    2:
    3:     const int DefaultSize = 10;
    4:
    5:     class Array
    6:     {
    7:     public:
    8:        // constructors
    9:        Array(int itsSize = DefaultSize);
    10:       Array(const Array &rhs);
    11:       ~Array() { delete [] pType;}
    12:
    13:       // operators
    14:       Array& operator=(const Array&);
    15:       int& operator[](int offSet);
    16:       const int& operator[](int offSet) const;
    17:
    18:       // accessors
    19:       int GetitsSize() const { return itsSize; }
    20:
    21:       // friend function
    22:      friend ostream& operator<< (ostream&, const Array&);
    23:
    24:     // define the exception classes
    25:       class xBoundary {};
    26:       class xSize
    27:       {
    28:       public:
    29:          xSize(int size):itsSize(size) {}
    30:          ~xSize(){}
    31:          virtual int GetSize() { return itsSize; }
    32:          virtual void PrintError() { cout << "Size error. Received: " <<
                 itsSize << endl; }
    33:       protected:
    34:          int itsSize;
    35:       };
    36:
    37:       class xTooBig : public xSize
    38:       {
    39:       public:
    40:          xTooBig(int size):xSize(size){}
    41:          virtual void PrintError() { cout << "Too big! Received: " <<
                 xSize::itsSize << endl; }
    42:       };
    43:
    44:       class xTooSmall : public xSize
    45:       {
    46:       public:
    47:          xTooSmall(int size):xSize(size){}
    48:          virtual void PrintError() { cout << "Too small! Received: " <<
                 xSize::itsSize << endl; }
    49:       };
    50:
    51:       class xZero  : public xTooSmall
    52:       {
    53:       public:
    54:          xZero(int size):xTooSmall(size){}
    55:          virtual void PrintError() { cout << "Zero!!. Received: " <<
                 xSize::itsSize << endl; }
    56:       };
    57:
    58:       class xNegative : public xSize
    59:       {
    60:       public:
    61:          xNegative(int size):xSize(size){}
    62:          virtual void PrintError() { cout << "Negative! Received: " <<
                 xSize::itsSize << endl; }
    63:       };
    64:
    65:    private:
    66:       int *pType;
    67:       int  itsSize;
    68:    };
    69:
    70:    Array::Array(int size):
    71:    itsSize(size)
    72:    {
    73:       if (size == 0)
    74:          throw xZero(size);
    75:       if (size > 30000)
    76:          throw xTooBig(size);
    77:       if (size <1)
    78:          throw xNegative(size);
    79:       if (size < 10)
    80:          throw xTooSmall(size);
    81:
    82:       pType = new int[size];
    83:       for (int i = 0; i<size; i++)
    84:         pType[i] = 0;
    85:    }
    86:
    87:    void main()
    88:    {
    89:
    90:       try
    91:       {
    92:          Array intArray(9);
    93:          for (int j = 0; j< 100; j++)
    94:          {
    95:             intArray[j] = j;
    96:             cout << "intArray[" << j << "] okay..." << endl;
    97:          }
    98:       }
    99:       catch (Array::xBoundary)
    100:      {
    101:         cout << "Unable to process your input!\n";
    102:      }
    103:      catch (Array::xSize& theException)
    104:      {
    105:         theException.PrintError();
    106:      }
    107:      catch (...)
    108:      {
    109:         cout << "Something went wrong, but I've no idea what!" << endl;
    110:      }
    111:      cout << "Done.\n";
    112:   }
Output:
    Too small! Received 9
    Done.
Analysis:

Listing 18.5 declares a virtual method in the xSize class, PrintError(), which prints an error message and the actual size of the class. This is overridden in each of the derived classes.

In line 103, the exception object is declared to be a reference. When PrintError() is called with a reference to an object, polymorphism causes the correct version of PrintError() to be invoked. The code is cleaner, easier to understand, and easier to maintain.

Using Exceptions with Templates

When creating exceptions to work with templates, you have a choice: you can create an exception for each instance of the template, or you can use Exception classes declared outside the template declaration. Listing 18.6 illustrates both approaches.

Listing 18.6 Using Exceptions with Templates

    1:     #include <iostream.h>
    2:
    3:     const int DefaultSize = 10;
    4:     class xBoundary {};
    5:
    6:     template <class T>
    7:     class Array
    8:     {
    9:     public:
    10:       // constructors
    11:       Array(int itsSize = DefaultSize);
    12:       Array(const Array &rhs);
    13:       ~Array() { delete [] pType;}
    14:
    15:       // operators
    16:       Array& operator=(const Array<T>&);
    17:       T& operator[](int offSet);
    18:       const T& operator[](int offSet) const;
    19:
    20:       // accessors
    21:       int GetitsSize() const { return itsSize; }
    22:
    23:       // friend function
    24:      friend ostream& operator<< (ostream&, const Array<T>&);
    25:
    26:     // define the exception classes
    27:
    28:       class xSize {};
    29:
    30:    private:
    31:       int *pType;
    32:       int  itsSize;
    33:    };
    34:
    35:    template <class T>
    36:    Array<T>::Array(int size):
    37:    itsSize(size)
    38:    {
    39:       if (size <10 || size > 30000)
    40:          throw xSize();
    41:       pType = new T[size];
    42:       for (int i = 0; i<size; i++)
    43:         pType[i] = 0;
    44:    }
    45:
    46:    template <class T>
    47:    Array<T>& Array<T>::operator=(const Array<T> &rhs)
    48:    {
    49:       if (this == &rhs)
    50:          return *this;
    51:       delete [] pType;
    52:       itsSize = rhs.GetitsSize();
    53:       pType = new T[itsSize];
    54:       for (int i = 0; i<itsSize; i++)
    55:          pType[i] = rhs[i];
    56:    }
    57:    template <class T>
    58:    Array<T>::Array(const Array<T> &rhs)
    59:    {
    60:       itsSize = rhs.GetitsSize();
    61:       pType = new T[itsSize];
    62:       for (int i = 0; i<itsSize; i++)
    63:          pType[i] = rhs[i];
    64:    }
    65:
    66:    template <class T>
    67:    T& Array<T>::operator[](int offSet)
    68:    {
    69:       int size = GetitsSize();
    70:       if (offSet >= 0 && offSet < GetitsSize())
    71:          return pType[offSet];
    72:       throw xBoundary();
    73:    }
    74:
    75:    template <class T>
    76:    const T& Array<T>::operator[](int offSet) const
    77:    {
    78:       int mysize = GetitsSize();
    79:       if (offSet >= 0 && offSet < GetitsSize())
    80:          return pType[offSet];
    81:       throw xBoundary();
    82:    }
    83:
    84:    template <class T>
    85:    ostream& operator<< (ostream& output, const Array<T>& theArray)
    86:    {
    87:       for (int i = 0; i<theArray.GetitsSize(); i++)
    88:          output << "[" << i << "] " << theArray[i] << endl;
    89:       return output;
    90:    }
    91:
    92:
    93:    void main()
    94:    {
    95:
    96:       try
    97:       {
    98:          Array<int> intArray(9);
    99:          for (int j = 0; j< 100; j++)
    100:         {
    101:            intArray[j] = j;
    102:            cout << "intArray[" << j << "] okay..." << endl;
    103:         }
    104:      }
    105:      catch (xBoundary)
    106:      {
    107:         cout << "Unable to process your input!\n";
    108:      }
    109:      catch (Array<int>::xSize)
    110:      {
    111:         cout << "Bad Size!\n";
    112:      }
    113:
    114:      cout << "Done.\n";
    115:   }
Output:
    Bad Size!
    Done.
Analysis:

The first exception, xBoundary, is declared outside the template definition in line 4. The second exception, xSize, is declared from within the definition of the template.

The exception xBoundary is not tied to the Template class, but can be used like any other class. xSize is tied to the template, and must be called based on the instantiated array. You can see the difference in the syntax for the two catch statements. Line 105 shows catch (xBoundary), but line 109 shows catch (Array<int>::xSize). Line 109 is tied to the instantiation of an integer array.

Using Exceptions without Errors

When C++ programmers get together for a virtual beer in the cyberspace bar after work, talk often turns to whether exceptions should be used for routine conditions. Some programmers maintain that exceptions should be reserved for those predictable but exceptional circumstances (hence the name!) that a programmer must anticipate, but that are not part of the routine processing of the code.

Other programmers point out that exceptions offer a powerful and clean way to return through many layers of function calls without danger of memory leaks. A frequent example is this: The user requests an action in a GUI environment. The part of the code that catches the request must call a member function on a dialog manager, which calls code that processes the request, which calls code that decides which dialog box to use, which calls code to put up the dialog box, which finally calls code that processes the user's input. If the user chooses Cancel, the code must return to the very first calling method, where the original request was handled.

One approach to this problem is to put a try block around the original call and catch CancelDialog as an exception, which can be raised by the handler for the Cancel button. This approach is safe and effective, but choosing Cancel is a routine circumstance, not an exceptional one.

This frequently becomes something of a religious argument, but there is a reasonable way to decide the question: Does use of exceptions in this way make the code easier to understand or harder? Are there fewer or more risks of errors and memory leaks? Will it be harder to maintain this code or easier? These decisions, like so many others, will require an analysis of the trade-offs; there is no single, obvious, correct answer.

Q&A

Q: Why bother with raising exceptions? Why not handle the error right where it happens?

A: Often, the same error can be generated in a number of different parts of the code. Exceptions enable you to centralize the handling of errors. Additionally, the part of the code that generates the error may not be the best place to determine how to handle the error.

Q: Why generate an object? Why not just pass an error code?

A: Objects are more flexible and powerful than error codes. They can convey more information, and the constructor/destructor mechanisms can be used for the creation and removal of resources that may be required to properly handle the exceptional condition.

Q: Why not use exceptions for nonerror conditions? Isn't it convenient to be able to express-train back to previous areas of the code, even when nonexceptional conditions exist?

A: Yes, some C++ programmers use exceptions for just that purpose. The danger is that exceptions might create memory leaks as the stack is unwound and some objects are inadvertently left in the free store. With careful programming techniques and a good compiler, this problem usually can be avoided. Otherwise, it is a matter of personal aesthetics; some programmers feel that exceptions should not be used for routine conditions.

Q: Does an exception have to be caught in the same place where the try block created the exception?

A: No. It is possible to catch an exception anywhere in the call stack. As the stack is unwound, the exception is passed up the stack until it is handled.

Workshop

The Workshop contains quiz questions to help solidify your understanding of the material covered, and exercises to provide you with experience in using what you have learned. Try to answer the quiz and exercise questions before checking the answers in Appendix A, and make sure that you understand the answers before going to the next chapter.

Quiz

  1. What is an exception?

  2. What is a try block?

  3. What is a catch statement?

  4. What information can an exception contain?

  5. When are exception objects created?

  6. Should you pass exceptions by value or by reference?

  7. Will a catch statement catch a derived exception if it is looking for the base class?

  8. If there are two catch statements, one for base and one for derived, which should come first?

  9. What does catch(...) mean?

[Click here for Answers]

Exercises

  1. Create a try block, a catch statement, and a simple exception.

  2. Modify the answer from exercise 1; put data into the exception, along with an accessor function; and use it in the catch block.

  3. Modify the class from exercise 2 to be a hierarchy of exceptions. Modify the catch block to use the derived objects and the base objects.

  4. Modify the program from exercise 3 to have three levels of function calls.

  5. BUG BUSTERS: What is wrong with the following code?
        class xOutOfMemory
        {
        public:
                xOutOfMemory( const String& message ) : itsMsg( message ){}
                ~xOutOfMemory(){}
                virtual const String& Message(){ return itsMsg};
        private:
                String itsMsg;          // assume you are using the string class as
                                           previously defined
        }
    
        main()
        {
                try {
                    char *var = new char;
                    if ( var == 0 )
                        throw xOutOfMemory();
                }
                catch( xOutOfMemory& theException )
                {
                    cout <<  theException.Message() << "\n";
                }
        }
    

Go to: Table of Contents | Next Page