11.2 The Try Block
The following small program exercises our class iStack and the pop() and push() member functions defined in the previous section. The for loop in main() iterates 50 times. It pushes on the stack each value that is a multiple of 3 — 3, 6, 9, and so on. Whenever the value is a multiple of 4, such as 4, 8, 12, and so on, it displays the contents of the stack. Whenever the value is a multiple of 10, such as 10, 20, 30, and so on, it pops the last item from the stack and then displays the contents of the stack again. How do we change main() to handle the exceptions thrown by the iStack member functions?
#include <iostream> #include "iStack.h" int main() { iStack stack( 32 ); stack.display(); for ( int ix = 1; ix < 51; ++ix ) { if ( ix % 3 == 0 ) stack.push( ix ); if ( ix % 4 == 0 ) stack.display(); if ( ix % 10 == 0) { int dummy; stack.pop( dummy ); stack.display(); } } return 0; }
A try block must enclose the statements that can throw exceptions. A try block begins with the try keyword followed by a sequence of program statements enclosed in braces. Following the try block is a list of handlers called catch clauses. The try block groups a set of statements and associates with these statements a set of handlers to handle the exceptions that the statements can throw. Where should we place a try block or try blocks in the function main() to handle the exceptions popOnEmpty and pushOnFull? Let's try this:
for ( int ix = 1; ix < 51; ++ix ) { try { // try block for pushOnFull exceptions if ( ix % 3 == 0 ) stack.push( ix ); } catch ( pushOnFull ) { ... } if ( ix % 4 == 0 ) stack.display(); try { // try block for popOnEmpty exceptions if ( ix % 10 == 0 ) { int dummy; stack.pop( dummy ); stack.display(); } } catch ( popOnEmpty ) { ... } }
The program as we have implemented it works correctly. Its organization, however, intermixes the handling of the exceptions with the normal processing of the program and thus is not ideal. After all, exceptions are program anomalies that occur only in exceptional cases. We want to separate the code that handles the program anomalies from the code that implements the normal manipulation of the stack. We believe that this strategy makes the code easier to follow and easier to maintain. Here is our preferred solution:
try { for ( int ix = 1; ix < 51; ++ix ) { if ( ix % 3 == 0 ) stack.push( ix ); if ( ix % 4 == 0 ) stack.display(); if ( ix % 10 == 0 ) { int dummy; stack.pop( dummy ); stack.display(); } } } catch ( pushOnFull ) { ... } catch ( popOnEmpty ) { ... }
Associated with the try block are two catch clauses that are capable of handling the exceptions pushOnFull and popOnEmpty that may be thrown from the iStack member functions push() and pop() called from within the try block. Each catch clause specifies within parentheses the type of exception it handles. The code to handle the exception is placed in the compound statement of the catch clause (between the curly braces). We examine catch clauses in greater detail in the next section.
The program control flow in our example is one of the following.
If no exception occurs, the code within the try block is executed and the handlers associated with the try block are ignored. The function main() returns 0.
If the push() member function called within the first if statement of the for loop throws an exception, the second and third if statements of the for loop are ignored, the for loop and the try block are exited, and the handler for exceptions of type pushOnFull is executed.
If the pop() member function called within the third if statement of the for loop throws an exception, the call to display() is ignored, the for loop and the try block are exited, and the handler for exceptions of type popOnEmpty is executed.
When an exception is thrown, the statements following the statement that throws the exception are skipped. Program execution resumes in the catch clause handling the exception. If no catch clause capable of handling the exception exists, program execution resumes in the function terminate() defined in C++ standard library. We further discuss the function terminate() in the next section.
A try block can contain any C++ statement — expressions as well as declarations. A try block introduces a local scope, and variables declared within a try block cannot be referred to outside the try block, including within the catch clauses. For example, we could rewrite our function main() so that the declaration of the variable stack appears within the try block. In this case, it is not possible to refer to stack in the catch clauses:
int main() { try { iStack stack( 32 ); // ok: declaration in try block stack.display(); for ( int ix = 1; ix < 51; ++ix ) { // same as before } } catch ( pushOnFull ) { // cannot refer to stack here } catch ( popOnEmpty ) { // cannot refer to stack here } // cannot refer to stack here return 0; }
It is possible to declare a function so that the entire body of the function is contained within the try block. In such a case, instead of placing the try block within the function definition we can enclose the function body within a function try block. This organization supports the cleanest separation between the code that supports the normal processing of the program and the code that supports the handling of the exceptions. For example:
int main() try { iStack stack( 32 ); stack.display(); for ( int ix = 1; ix < 51; ++ix ) { // same as before } return 0; } catch ( pushOnFull ) { // cannot refer to stack here } catch ( popOnEmpty ) { // cannot refer to stack here }
Notice that the try keyword comes before the opening brace of the function body and the catch clauses are listed after the function body's closing brace. With this code organization, the code that supports the normal processing of main() is placed within the function body, clearly separated from the code that handles the exceptions in the catch clauses. However, variables declared within main()'s function body cannot be referred to within the catch clauses.
A function try block associates a group of catch clauses with a function body. If a statement within the function body throws an exception, the handlers that follow the function body are considered to handle the exception. Function try blocks are particularly useful with class constructors. We will reexamine function try blocks in this context in Chapter 19.
Exercise 11.3
Write a program that defines an IntArray object (where IntArray is the class type defined in Section 2.3) and performs the following actions. We have three files containing integer values.
Read the first file and assign the first, third, fifth, ..., nth value read (where n is an odd number) to the IntArray object; then display the content of the IntArray object.
Read the second file and assign the fifth, tenth, ..., nth value read (where n is a multiple of 5) to the IntArray object; then display the content of the IntArray object.
Read the third file and assign the second, fourth, sixth..., nth value read (where n is an even number) to the IntArray object; then display the content of the Int- Array object.
Use the IntArray operator[]() defined in Exercise 11.2 to store values into and read values from the IntArray object. Because operator[]() may throw an exception, use one or more try blocks and catch clauses in your program to handle the possible exceptions thrown by operator[]() . Explain the reasoning behind where you located the try blocks in your program.