In This Chapter
Introducing an exceptional way of handling program errors
Finding what's wrong with good ol' error returns
Examining throwing and catching exceptions
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 new, improved mechanism for capturing and handling errors called exceptions. An exception is defined as "a case in which a rule or principle does not apply." Exception is also defined as an objection to something. Either definition works: An exception is an unexpected (and presumably objectionable) condition that occurs during the execution of the program.
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) throw (string) {
// 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 system("PAUSE"); 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 second call to factorial()
is not performed.
The declaration for factorial()
also announces to the compiler that it may throw a string
object somewhere under the right conditions. This is a relatively new, and so far optional, feature. If absent, the function may throw any object that it wants. If present, the function can throw only one of the types of objects included in the declaration.
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's been done for ages.
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 the negative return value trick won't work here — logarithms can be either negative or positive.
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 your 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.
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 correct the problem need to catch the exception.
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; it also // shows how the throw() clause is used in // the function declaration
#include <cstdio> #include <cstdlib> #include <iostream> using namespace std; // prototypes of some functions that we will need later void f1() throw(); void f2(); void f3() throw(int); 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 system("PAUSE"); return 0; } // f1 -an empty throw() clause in the declaration of this // function means that it does not throw an exception void f1() throw() { 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; }
} // f2 - the absence of a throw() clause in the // declaration of this function means that it may // throw any kind of object void f2() { try { Obj c('c'), f3(); } catch(string msg) { cout << "String catch" << endl; } } // f3 - this function may throw an int object void f3() throw(int) { 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 any key 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 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.
Notice that f2()
does not declare the types of objects it might throw but the functions f1()
and f3()
do. Why is this descriptor optional? The throw
specification in the declaration was added to the definition of the language long after the exception mechanism itself. First, the new standard wants to remain compatible with the existing C++ code that doesn't declare the types of objects thrown. In addition, it's not clear that forcing the user to declare the type of object thrown is a good idea.
If I wanted to include the type of objects thrown by f2()
, I would have to declare it as follows:
void f2() throw(int);
Even though f2()
doesn't throw an int
directly, it does so indirectly by calling f3()
and not catching the int
thrown there. In a real-world program, it can get complicated trying to keep track of all the objects that a function could possibly 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 << " " << "in function " << funcName << "() " << "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 system("PAUSE"); 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.
__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_ProgramsChap25CustomExceptionClassmain.cpp line #53 Press any key to continue ...
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.
18.188.61.81