Wednesday, May 5, 2010

Error handling

Unlike C, C++ has a built in mechanism for dealing with the unexpected, and it's called exception handling (Note, if you're experienced in OS/2 programming in other languages, you might have used OS/2 exception handlers; this is not the same thing, this is a language construct, not an operating system construct.) Exceptions are a function's means of telling its caller that it cannot do what it's supposed to do. The classic C way of doing this, is using error codes as return values, but return values can easily be ignored, whereas exceptions can not. Exceptions also allow us to write pure functions, that either succeed in doing the job, thus returning a valid value, or fail and terminate through an exception. This last sentence is paramount to any kind of error handling. For a function there are only two alternatives; it succeeds with doing its job, or it does not. There's no "sort of succeeded." When a function succeeds, it returns as usual, and when it fails, it terminates through an exception. The C++ lingo for this termination is to "throw an exception." You can see this as an incredibly proud and loyal servant, that does what you tell it to, or commits suicide. When committing suicide, however, it always leaves a note telling why. In C++, the note is an instance of some kind of data, any kind of data, and being the animated type, the function "throws" the data towards its caller, not just leaves it neatly behind. Let's look at an example of exception handling, here throwing a character string:
#include
int divide(int divisor, int dividend) throw (const char*);
// Divides divisor with dividend.
// Precondition, dividend != 0
int main(void)
{
try {
int result = divide(50,2);
cout << "divide(" << 50 << ", " << 2
<< ") yields " << result << endl;
result = divide(50,0);
cout << "divide(" << 50 << ", " << 0
<< ") yields " << result << endl;
}
catch (const char* msg) {
cout << "Oops, caught: " << msg << endl;
}
return 0;
}
int divide(int divisor, int dividend) throw (const char*)
{
if (dividend == 0)
throw (const char*)"Division by zero attempted";
// Here we don't have to worry about dividend being zero
return divisor/dividend;
}
This mini program shows the mechanics of C++ exception handling. The function prototype for "divide" at //**1 adds an exception specification "throw (const char*)". Exceptions are typed, and a function may throw exceptions of different types. The exception specification is a comma separated list, showing what types of exceptions the function may throw. In this case, the only thing this function can throw is character strings, specified by "const char*".
Any attempt to do something, when you want to find out if it succeeded or not, must be enclosed in a "try/catch" block. At //**2 we see the "try" block. A try block is *always* followed by one or several "catch" blocks (//**3). If something inside the "try" block (in this case, a call to "divide") throws an exception, execution immediately leaves the "try" block and enters the "catch" block with the same type as the exception thrown. Here, when "divide" is called with a dividend of 0, a "const char*" is thrown, the "try" block is left and the "catch" block entered. If no "catch" block matches the type of exception thrown, execution leaves the function, and a matching "catch" block (if any) of its caller is entered. If no matching "catch" block is found when "main" is reached, the program terminates.
When a function finds that it cannot do whatever it is asked to do, it throws an exception, as shown at //**4. If the exception thrown does not match the exception specification of the function, the program terminates.
Compile and test the program:
[d:\cppintro\lesson1]gcc -fhandle-exceptions excdemo.cpp -lstdcpp
[d:\cppintro\lesson1]excdemo.exe
divide(50, 2) yields 25
Oops, caught: Division by zero attempted
(If you're using VisualAge C++, you don't need any special compiler flags to enable exception handling, and for Watcom C++, use /xs.)
This exception handling can be improved, though. As I mentioned above, exceptions are typed. This is a fact that can, and should, be exploited. In the program above, we have little information, other than that something's gone wrong, and that we can see exactly what by reading the string. The program, however, cannot do much about the error other than printing a message, and the message itself is not very informative either, since we don't know where the error originated from anyway. If we instead create a struct type, holding more information, we can do much better. If we create different struct types for different kinds of errors, we can catch the different types (separate "catch" blocks) and take corresponding action. Here's an attempt at improving the situation:
#include
// Simple math program, demonstrating different kinds of
// exception types.
// First 3 math error structs, overflow, underflow and
// zero_divide.
struct overflow // Unlike the case for C, in C++
{ // you don't need to "typedef" the
const char* msg; // struct to be able to access it
const char* function; // without the "struct" keyword.
const char* file;
unsigned long line;
};
struct underflow
{
const char* msg;
const char* function;
const char* file;
unsigned long line;
};
struct zero_divide
{
const char* msg;
const char* function;
const char* file;
unsigned long line;
};
unsigned add(unsigned a, unsigned b) throw (overflow);
// precondition a+b representable as unsigned.
unsigned sub(unsigned a, unsigned b) throw (underflow);
// precondition a-b representable as unsigned.
unsigned divide(unsigned a, unsigned b)
throw (zero_divide);
// Precondition b != 0
unsigned mul(unsigned a, unsigned b) throw (overflow);
// Precondition a*b representable as unsigned.
unsigned calc(unsigned a, unsigned b, unsigned c)
throw (overflow, underflow, zero_divide);
// Calculates a*(b-c)/b with functions above.
void printError(const char* func,
const char* file,
unsigned line,
const char* msg);
// Print an error message.
int main(void)
{
cout << "Will calculate (a*(b-c))/b" << endl;
int v1;
cout << "Enter a:";
cin >> v1;
int v2;
cout << "Enter b:";
cin >> v2;
cout << "Enter c:";
int v3;
cin >> v3;
for (;;)
{
try {
int result = calc(v1, v2, v3);
cout << "The result is " << result << endl;
return 0;
}
catch (zero_divide& z)// zero_divide& z means
{ // "z is a reference to a
// zero_divide". A reference, in
// this case, has the semantics
// of a pointer, that is, it
// refers to a variable located
// somewhere else, but
// syntactically it's like we
// were dealing with a local
// variable. More about
// references later.
cout << "Division by zero in:" << endl;
printError(z.function,
z.file,
z.line,
z.msg);
cout << endl;
cout << "Enter a new value for b:";
cin >> v2;
}
catch (underflow& u) //** reference
{
cout << "Underflow in:" << endl;
printError(u.function,
u.file,
u.line,
u.msg);
cout << endl;
cout << "Enter a new value for c:";
cin >> v3;
}
catch (overflow& o) //** reference
{
cout << "Overflow in:" << endl;
printError(o.function,
o.file,
o.line,
o.msg);
cout << endl;
cout << "Enter a new value for a:";
cin >> v1;
}
catch (...) // The ellipsis (...) matches any type.
{ // The disadvantage is that you cannot
// find out what type it was you caught.
cout << "Severe error: Caught unknown exception"
<< endl;
return 1;
}
}
}
unsigned add(unsigned a, unsigned b) throw (overflow)
{
unsigned c = a+b;
if (c < a c < b) { // If c is less than either a or
overflow of; // b, the value "wrapped"
of.function = "add";
of.file = __FILE__; // standard macro containing the
// name of the C++ file.
of.line = __LINE__; // standard macro containing the
// line number.
of.msg = "Overflow in addition";
throw of;
}
return c;
}
unsigned sub(unsigned a, unsigned b) throw (underflow)
{
unsigned c = a-b;
if (c > a) { // If c is greater than a
// the value "wrapped"
underflow uf;
uf.function ="sub";
uf.file = __FILE__;
uf.line = __LINE__;
uf.msg = "Underflow in subtraction";
throw uf;
}
return c;
}
unsigned divide(unsigned a, unsigned b)
throw (zero_divide)
{
if (b == 0) {
zero_divide zd;
zd.function = "divide";
zd.file = __FILE__;
zd.line = __LINE__;
zd.msg = "Division by zero";
throw zd;
}
return a/b;
}
unsigned mul(unsigned a, unsigned b) throw (overflow)
{
unsigned c = a*b;
// If c is less than either a or b, and neither of
// a or b is 0, then the value "wrapped".
if (a != 0 && b != 0 && (c < a c < b))
{
overflow of;
of.function = "mul";
of.file = __FILE__;
of.line = __LINE__;
of.msg = "Overflow in mul";
throw of;
}
return c;
}
unsigned calc(unsigned a, unsigned b, unsigned c)
throw (overflow, underflow, zero_divide)
{
// Calculates a*(b-c)/b with functions above.
try {
// We can only hope...
unsigned result = divide(mul(a,sub(b,c)),b);
return result;
}
catch (zero_divide& zd) { //** reference
zd.function = "calc"; // We can alter the struct to
// allow better tractability of
// the error.
throw; // An empty "throw" means "throw the exception
// just caught. This is only legal in a catch
// block.
}
catch (underflow& uf) {
uf.function = "calc";
throw;
}
catch (overflow& of) {
of.function = "calc";
throw;
}
}
void printError(const char* func,
const char* file,
unsigned line,
const char* msg)
{
cout << " " << func << endl
<< " " << file << '(' << line << ')'
<< endl << " \"" << msg << "\"" << endl;
}
If compiled and run:
[d:\cppintro\lesson1]icc /Q excdemo2.cpp
[d:\cppintro\lesson1]excdemo2.exe
Will calculate (a*(b-c)/b
Enter a:23
Enter b:34
Enter c:45
Underflow in:
calc
excdemo2.cpp(122)
"Underflow in subtraction"
Enter a new value for c:21
The result is 8
What do you think of this? Do you think the code is messy? How would you have implemented the same functionality without exception handling? The code is a bit messy, but part of the mess will be removed as you learn more about C++, and the other part is due to handling the error situations. It's frightening how frequent lack of error handling is, but as I read in a calendar "Unpleasant facts [errors] don't cease to exist just because you chose to ignore them." Also, errors are easier to handle if you take them into account in the beginning, instead of, as I've seen far too often, add error handling afterwards. There are problems with the code above, as will be mentioned soon, but one definite advantage gained by using exceptions is that the code for error handling is reasonably separated from the parts that does it's job. This separation will become even clearer as you learn more C++.

No comments:

Post a Comment