In the previous lesson on introduction to exceptions, we talked about how using return codes causes you control flow and error flow to be intermingled, constraining both. Exceptions in C++ are implemented using three keywords that work in conjunction with each other:
throw,
try, and
catch.
Throwing exceptions
We use signals all the time in real life to note that particular events have occurred. For example, during American football, if a player has committed a foul, the referee will throw a flag on the ground and whistle the play dead. A penalty is then assessed and executed. Once the penalty has been taken care of, play generally resumes as normal.
In C++, a
throw statement is used to signal that an exception or error case has occurred (think of throwing a penalty flag). Signaling that an exception has occurred is also commonly called
raising an exception.
To use a throw statement, simply use the throw keyword, followed by a value of any data type you wish to use to signal that an error has occurred. Typically, this value will be an error code, a description of the problem, or a custom exception class.
Here are some examples:
2 | throw ENUM_INVALID_INDEX; |
3 | throw "Can not take square root of negative number" ; |
5 | throw MyException( "Fatal Error" ); |
Each of these statements acts a signal that some kind of problem that needs to be handled has occurred.
Looking for exceptions
Throwing exceptions is only one part of the exception handling process. Let’s go back to our American football analogy: once a referee has thrown a penalty flag, what happens next? The players notice that a penalty has occurred and stop play. The normal flow of the football game is disrupted.
In C++, we use the
try keyword to define a block of statements (called a
try block). The try block acts as an observer, looking for any exceptions that are thrown by statements within the try block.
Here’s an example of an try block:
Note that the try block doesn’t define HOW we’re going to handle the exception. It merely tells the program, “Hey, if you see an exception in the following code, grab it!”.
Handling exceptions
Finally, the end of our American football analogy: After the penalty has been called and play has stopped, the referee assesses the penalty and executes it. In other words, the penalty must be handled before normal play can resume.
Actually handling exceptions is the job of the catch block(s). The
catch keyword is used to define a block of code (called a
catch block) that handles exceptions for a single data type.
Here’s an example of a catch block:
4 | cerr << "We caught an exception of type int" << endl; |
Try blocks and catch blocks work together — A try block detects any exceptions that are thrown by statements within the try block, and routes them to the appropriate catch block for handling. A try block must have at least one catch block attached to it, but may have multiple catch blocks listed in sequence:
11 | cerr << "We caught an exception of type int" << endl; |
16 | cerr << "We caught an exception of type double" << endl; |
Putting throw, try, and catch together
Running the above try/catch block would produce the following result:
We caught an exception of type int
A throw statement was used to raise an exception with the value -1, which is of type int. The try block routed the int exception to the catch block that handles exceptions of type int, which then printed the error message.
Exception handling behind the scenes
Let’s talk about what happens behind the scenes in a little more detail. Exception handling is actually quite simple, and the following two paragraphs are all you really need to know about it:
When an exception is raised (using
throw), execution of the program immediately jumps to the nearest enclosing
try block (propagating up the stack if necessary). If any of the
catch handlers attached to the try block handle that type of exception, that handler is executed and the exception is considered handled.
If no appropriate catch handlers exist, execution of the program propagates to the next enclosing try block. If no appropriate catch handlers can be found before the end of the program, the program will fail with an exception error.
That’s really all there is to it. The rest of this chapter will be dedicated to showing examples of these principles at work.
Exceptions are handled immediately
Here’s a short program that demonstrates how exceptions are handled immediately:
06 | cout << "This never prints" << endl; |
10 | cerr << "We caught a double of value: " << dX << endl; |
This program is about as simple as it gets. Here’s what happens: the throw statement is the first statement that gets executed — this causes an exception of type double to be raised. Execution immediately moves to the nearest enclosing try block, which is the only try block in this program. The catch handlers are then checked to see if any handlers matche. Our exception is of type double, so we’re looking for a catch handler of type double. We have one, so it executes.
Consequently, the result of this program is as follows:
We caught a double of value: 4.5
Note that the cout statement never executed, because the exception caused the execution path to change to the exception handler for doubles.
A more realistic example
Let’s take a look at an example that’s not quite so academic:
01 | #include "math.h" // for sqrt() function |
06 | cout << "Enter a number: " ; |
14 | throw "Can not take sqrt of negative number" ; |
17 | cout << "The sqrt of " << dX << " is " << sqrt (dX) << endl; |
19 | catch ( char * strException) |
21 | cerr << "Error: " << strException << endl; |
In this code, the user is asked to enter a number. If they enter a positive number, the if statement does not execute, no exception is thrown, and the square root of the number is printed. Because no exception is thrown in this case, the code inside the catch block never executes. The result is something like this:
Enter a number: 9
The sqrt of 9 is 3
If the user enters a negative number, we throw an exception of type char*. Because we’re within a try block and a matching exception handler is found, control immediately transfers to the char* exception handler. The result is:
Enter a number: -4
Error: Can not take sqrt of negative number
By now, you should be getting the basic idea behind exceptions. In the next lesson, we’ll do quite a few more examples to show how flexible exceptions are.
Multiple statements within a try block
In the lesson on the need for
exceptions, we showed an example of one case where return codes don’t work very well:
01 | std::ifstream fSetupIni( "setup.ini" ); |
03 | return ERROR_OPENING_FILE; |
07 | if (!ReadParameter(fSetupIni, m_nFirstParameter)) |
08 | return ERROR_PARAMETER_MISSING; |
09 | if (!ReadParameter(fSetupIni, m_nSecondParameter)) |
10 | return ERROR_PARAMETER_MISSING; |
11 | if (!ReadParameter(fSetupIni, m_nThirdParameter)) |
12 | return ERROR_PARAMETER_MISSING; |
In this code, ReadParameter() is returning a boolean value indicating success or failure. We end up having to check the return code from each call to ReadParameter() to ensure that it succeeded before proceeding. This leads to code that is messy and redundant.
Let’s rewrite this snippet of code using a new version of ReadParameter() throws an int exception on failure instead of returning a boolean value:
01 | std::ifstream fSetupIni( "setup.ini" ); |
03 | return ERROR_OPENING_FILE; |
09 | m_nFirstParameter = ReadParameter(fSetupIni); |
10 | m_nSecondParameter = ReadParameter(fSetupIni); |
11 | m_nThirdParameter = ReadParameter(fSetupIni); |
15 | return ERROR_PARAMETER_MISSING; |
Note how much easier this is to read! If any of the calls to ReadParameter() throws an exception, that exception will be caught and routed to the int exception handler, which returns an error enum to the caller.
No comments:
Post a Comment