Handling Errors — Exceptions - Security - C++ For Dummies (2014)

C++ For Dummies (2014)

Part V

Security

Chapter 24

Handling Errors — Exceptions

In This Chapter

arrow Introducing an exceptional way of handling program errors

arrow Finding what’s wrong with good ol’ error returns

arrow Examining throwing and catching exceptions

arrow 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 mechanism for capturing and handling errors called exceptions. The handling of error conditions with exceptions is the subject of this chapter.

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)
{
// 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
cout << "Press Enter to continue..." << endl;
cin.ignore(10, '\n');
cin.get();
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 third call to factorial() is not performed.

image Through a not-so-clever feature called an exception specification, you can add the type of objects that factorial() throws to its declaration. At one time, someone thought this would be a good idea, but times change. Exception specifications were never mandatory and have been deprecated in the 2011 standard. Exception specifications are not presented in this book.

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 was done for ages. (“If it was good enough for my grandpa…”)

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 logarithms can be either negative or positive. There's no value that a logarithm function can't return.

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 its 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 and it's all mixed together.

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 handle 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
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;

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

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
cout << "Press Enter to continue..." << endl;
cin.ignore(10, '\n');
cin.get();
return 0;
}

void f1()
{
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;
}
}

void f2()
{
try
{
Obj c('c');
f3();
}
catch(string msg)
{
cout << "String catch" << endl;
}
}

void f3()
{
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 Enter 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 a 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.

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 << "\n"
<< "in function " << funcName << "()\n"
<< "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
cout << "Press Enter to continue..." << endl;
cin.ignore(10, '\n');
cin.get();
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.

image __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_Programs_from_Book\Chap24\CustomExceptionClass\main.cpp line #52
Press Enter 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.