Thursday, November 16, 2006
Exceptions

Topic

- Until Exceptions were added to C++, each program handled errors in its own way.  For particularly large projects, the error handling code would consume a major percentage of the total code.
- The fundamental problem is that the really tough errors usually occur deep within a series of nesting function calls, but the ability to handle the error usually is only possible in one of the upper calling functions, that have a broader access to the overall program.
- C++, and most programming languages, needed a good way for a deep stack of function calls to interrupt on an unusual error and unwind back up the function calls until reaching an upper function that can then recover from the error.  Exceptions can do exactly this.

- Exceptions have been a very successful addition (in the 1990's) to C++ that have been included in Java and C# nearly unchanged.  The verdict from the programming community is positive.
- Exceptions should only be used for unusual or really tough errors that are not a normal part of your code's logic. 
- Use normal error catching techniques, such as returning a SUCCESS or FAILURE constant from a function to the calling function, to handle normal conditions when the logic of your program detects a common problem, such as bad input from the user, or an illogical value passed as a parameter to a function.
- Exceptions should only be used to handle rare and extraordinary problems that normally should not occur, such as an expected closing or disappearance of a file, or taking the square root of a negative value that is carefully and always kept positive by the logic of your code.

throw syntax

- Exceptions are objects of a class that normally should inherit from Standard Template Library class exception or its derived classes  runtime_error  or  logic_error.   They can, however, be any object or variable, including intrinsic data type such as int or double.  (code adapted from Deitel and Deitel)

#include <stdexcept> // stdexcept header file contains runtime_error
using namespace std; 

class DivideByZeroException : public runtime_error
{
public:
    DivideByZeroException() : runtime_error( "attempted to divide by zero" )
        { // pointers or references to needed recovery objects usually set here }
};


- Notice the inline Constructor is the only function necessary.  Exceptions normally are small objects.


- When a function encounters an encounters an error, it "throws" an exception.  That means it creates an exception object and uses the keyword   throw   to send that object back up to the calling functions.
- A throw statement looks like a return statement, except the variable or object following the throw keyword must be caught by a catch statement or the program will crash.
- Execution of the function stops immediately at the throw statement.   The rest of the function is not executed.  Anything implemented by the code following the throw statement will not be done.
 

#include "DivideByZeroException.h"
double safeintegerdivision( int numerator, int denominator )
{
   if ( denominator == 0 )
      throw DivideByZeroException();
   return (double)numerator / denominator;
}

- Notice that the function simply throws the exception, without additional code to warn the calling function of that possibility.  This is not recommended, and we will see the improvement below.  With code written this way the calling functions are not required to contain any code to catch the exception, which usually causes the program to crash after the exception is propagated all the way up to int main().

- Now suppose we have a series of functions that perform a task using nested calls:

#include <math>

double average(int array[], int size)
{
    int total = 0; 
    for (int i = 0; i < size; i++)
        total += array[i];

    return safeintegerdivision(total / size);   
}

double rootmeansquare(int array[], int size)
{
    int* rmsarray = new int[size];

    for (int i = 0; i < size; i++)
        rmsarray[i] = array[i] * array[i];

    double rms = sqrt(average(rmsarray, size));

    delete[] rmsarray;
   
    return rms;  
}

void importantcalculation()
{
    // code to initialize measurements array
    rootmeansquareresult = rootmeansquare(measurements, measurementsize);
    // code to use rootmeansquareresult,
    //   dependant on successful execution of rootmeansquare

    // additional code, not dependant on the success of the rootmeansquare function
}

- notice  importantcalculation  calls   rootmeansquare  which calls  average  which calls  safeintegerdivision.  If the DivideByZeroException is thrown in safeintegerdivision, it can not be handled by  average  or  rootmeansquare, because the real source of the problem is up in importantcalculation which should not have called rootmeansquare with a measurements array of size zero.
- One way to protect against a divide by zero problem would be to write a protective if around the
rootmeansquare function call:

if (measurementsize != 0)
   rootmeansquareresult = rootmeansquare(measurements, measurementsize);
else
   // handle error in some way


- The problem with this protective if statement is it adds code that must be executed every time, even if the function is carefully designed to never get to this call if the measurements array is zero size.  A lot of protective code such as this bloats good code and slows down execution.
- Using exception handling improves the code by isolating the problems that normally should never occur into code that is both clear and efficient.  Exception handling code is never executed unless the problem actually occurs.  If the exception never happens, and it should never happen, the code runs as if there is no exception code written.

- Let's look at what happens If the exception actually occurs.

1.
 safeintegerdivision stops immediately after the throw DivideByZeroException(). The division numerator / denominator never occurs, so there is no program crash.

2.
 average receives the exception exactly when safeintegerdivision is called.  Since it has no code to handle the exception, it immediately and automatically re-throws the exception to its calling function.  The return statement never completes, and no value is returned to the calling function, other than the DivideByZeroException. 

3.  Likewise
rootmeansquare receives the exception exactly when average is called.  Since it also has no code to handle the exception, the exception is re-thrown another time to rootmeansquare's calling function, importantcalculation.  The rms variable is never assigned a value.  Notice also the delete[] rmsarray is never executed, causing a memory leak. 

4.  Finally
importantcalculation  gets the DivideByZeroException.  It is the function that can recover from the problem, since it should never have sent importantcalculation  an zero sized array in the first place.  However, it also has no code that can catch the exception, so it automatically re-throws the exception to its calling function, which really should not have any responsibility to catch or correct the exception.  When the exception reaches int main() which certainly should not be responsible for correcting the error, the program crashes.  Obviously not good. 

 

try { } catch ( )  syntax

- Let's improve the above code so that the DivideByZero exception is properly handled.

- First of all,
class DivideByZeroException does not need to be changed.  It is correct as written. 

// no change
class DivideByZeroException : public runtime_error
{
public:
    DivideByZeroException() : runtime_error( "attempted to divide by zero" )
        { // pointers or references to needed recovery objects usually set here }
};
 
- However, for function safeintegerdivision we need to add code to warn the compiler to check for code in the calling functions that will catch this exception. 
 

double safeintegerdivision( int numerator, int denominator ) throw (DivideByZeroException)
{
   if ( denominator == 0 )
      throw DivideByZeroException();
   return (double)numerator / denominator;
}

- Notice that the function declares what kind of exception it throws in the function header (and in the prototype).  Declaring the exception in the function header is not required, but is strongly recommended.
- Also notice the required parentheses after
throw  in the function header, even though parentheses are not required in the throw  statement in the executable code in the function body.  This is a questionable choice of syntax that Java corrected by using two key words,  throw  for executable code inside a function, and throws  in the function header.  Neither use parentheses.  The Java syntax reads more naturally than the C++ syntax.

- Since 
average can not do anything to correct the problem, it merely re-throws the exception.  Explicitly stating this in the code, however, is necessary for the same compiler checks as above.

double average(int array[], int size)
throw (DivideByZeroException)
{
    int total = 0; 
    for (int i = 0; i < size; i++)
        total += array[i];

    return safeintegerdivision(total / size);   
}
 

- rootmeansquare has a more complicated problem -- the memory leak caused by delete[] never executing if there is an exception.  Here is the solution for that problem:


double rootmeansquare(int array[], int size)
throw (DivideByZeroException)
{
    int* rmsarray = new int[size];
    double rms = 0.0;

    for (int i = 0; i < size; i++)
        rmsarray[i] = array[i] * array[i];

    try
    {
       
rms = sqrt(average(rmsarray, size));
    }
    catch (DivideByZeroException ex)
    {
       
delete[] rmsarray;
        throw;
    }

    return rms;  
}
 

- There are a number of important changes here:

-  First, notice 
rootmeansquare is also defined with a throw (DivideByZeroException) in its function header and prototype, for the same reasons as above.

-  Second, the dangerous function call, which might throw an exception, is put into a
try {  } block.  If an exception is generated by the function call, the try block catches it.  Execution immediately jumps to the catch ( ) statement just below the try block.  catch statements are similar to else  statements, in that they must always immediately follow a try block, with no intervening code.  However, you can have multiple catch statements after a single try block.  That will be explained below.

- Third, the 
catch statement contains code to partially solve the problems caused by the exception.  It deletes the dynamic array it created, preventing a memory leak. 

- Fourth, since
rootmeansquare cannot fix the original problem, a zero size array, it explicitly re-throws the exception to its calling function with the   throw; statement.  Notice that this is the third different syntatical use of the keyword  throw   If  throw is used by itself with only a semi-colon following, it means "re-throw the current exception". 

- Finally, the declaration of the variabe 
catch sta a  double must be moved to the top of the function so that its scope is the entire function.  If the declaration of  rms is left inside the try block, then its scope would be within the try block's curly braces only.  The final  return rms; statement would give a "undeclared identifier" error because rms would not be defined outside the try block's parentheses.

- One more detail, since the
catch statement contains a  throw;  statement, execution of the function will stop immediately at the  throw. The  return rms; statement will never be executed.  This is different from the next try block example.

 

- Lastly, let's improve the importantcalculation function so that it fully handles the exception.  Here is the improved code:

void importantcalculation()
{
    // code to initialize measurements array
    try
    {

        rootmeansquareresult = rootmeansquare(measurements, measurementsize);
        // code to use rootmeansquareresult
        // all code that depends on a successful return from the rootmeansquare
        //   function must go here or you will get a compiler error.
    }

    catch(DivideByZeroException)
    {
        // code to completely recover from the exception here
        // this code should not re-throw the DivideByZeroException because
        //   it is not the calling function's responsibility to handle this error.
    }

    // additional code, not dependant on the success of the rootmeansquare function
}

- Some important details:
- The syntax for catching the exception is exactly the same.
-  All code that depends on the result of the dangerous function call must be included in the try block, after the function call.  If the call is successful, then this code will be executed.  The the call throws an exception, this code will be skipped as execution jumps immediately to the catch statement.
-  In this case, the catch statement completely handles the problem.  It does not re-throw the exception.  Therefore execution continues after the catch statement and the 
// addtional code...  is fully executed.  If a catch statement does not have a throw; statement inside, then the problem is considered fully handled by the catch statement, and execution continues normally from the closing curly brace of the catch statement.
- Since this function completely handles the exception, and does not re-throw it, no
throw (DivideByZeroException) is needed in the function header, and the calling functions will not need any further try-catch blocks for this exception.


Multiple catch statements

- If your code calls a number of functions that can throw different exceptions, you can put all of the code into a single try block and have multiple catch statements following the one try block. 
- There are two important points to remember, however.
-  1. catch statements will be executed in the order written, until all of the exceptions are handled by one of the catch statements.
-  2. Most important: A catch statement catches a specific class of exception, and all exceptions derived from that exception.  If a catch statement catches a base class exception, then a following catch statement designed to catch a derived class exception will never execute.

For instance
.  

// prototypes
double safeintegerdivision(int, int) throw (DivideByZeroException);
double safesquareroot(int) throw (SquareRootOfNegativeException);
void someothercalculation(int, int*) throw (runtime_error);

void importantcalculation()
{
    // additional code here
    try
    {
        z = safeintegerdivision(x, y);
        sr = safesquareroot(v);
        someothercalculation(z, &sr);  
        // additional code using z or sr here

    }
    catch (DivideByZeroException ex)
    {
        // code to handle this exception
    }
    catch (SquareRootOfNegativeException ex)
    {
        // code to handle this exception
    }
    catch (runtime_error ex)
    {
        // code to handle any exception that
        //  is not a DivideByZeroException or
        //  a SquareRootOfNegativeException
    }
    catch (...)
    {
        // this will catch any exception.
        //   it is not recommended to use
        //   this syntax, except maybe at the
        //   very top of your program's function
        //   call stack, to make sure the program
        //   does not crash from an unexpected
        //   and unknown exception.
    }   
}

- It is assumed here that DivideByZeroException and  SquareRootOfNegativeException  both inherit from the  runtime_error class.
- Notice the order of the catch statements.  It would not make sense for the
catch (runtime_error ex) statement to come before the catch(DivideByZeroException ex)  statement, since runtime_error is the base class of DivideByZeroException.  The DivideByZeroException code would never execute, since that exception would always be caught by the runtime_error catch statement, if the runtime_error catch comes before the DivideByZeroException catch statement.


Readings

Deitel and Deitel Fifth Edition, Chapter 16.?, 16.?, 16.?

 

Back to Csc 125 Programming in C++
  Scott Badman  Office:  B132  Phone:  353-2250  sbadman@parkland.edu  

Parkland College, 2400 W. Bradley Avenue, Champaign, IL 61821