|
| Thursday, November 16, 2006 |
| Exceptions |
- 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 |