I l@ve RuBoard Previous Section Next Section

7.3 Trying for an Exception

Catch clauses are associated with try blocks. A try block begins with the try keyword followed by a sequence of program statements enclosed in braces. The catch clauses are placed at the end of the try block, and they represent the exceptions that are handled if an exception is thrown during execution of the statements within the try block.

For example, the following function looks for elem within a range of elements marked by first, last. The iteration over the range can potentially result in an iterator_overflow exception being thrown, so we place that code within a try block followed by a catch clause that contains an iterator_overflow exception declaration:



bool has_elem( Triangular_iterator first, 


               Triangular_iterator last, int elem ) 


{ 


    bool status = true; 





    try 


    { 


        while ( first != last ) 


        { 


                if ( *first == elem ) 


                     return status; 


                ++first; 


        } 


    } 


    // only exceptions of type iterator_overflow 


    // are caught if thrown while the code 


    // sequence within the try block is executed 


    catch( iterator_overflow &iof ) 


    { 


        log_message( iof.what_happened() ); 


        log_message( "check if iterators address same container" ); 


    } 





    status = false; 


    return status; 


} 

The expression



*first 

invokes the overloaded dereference operator:



inline int Triangular_iterator:: 


operator*() 


{ 


    check_integrity(); 


    return Triangular::_elems[ _index ]; 


} 

That in turn invokes check_integrity():



inline void Triangular_iterator:: 


check_integrity() 


{ 


    if ( _index > Triangular::_max_elems ) 


         throw iterator_overflow( _index, Triangular::_max_elems ); 


    // ... 


} 

Let's say that somehow the _index value of last is greater than _max_elems so that at some point the test within check_integrity() evaluates as true and the exception is thrown. What happens?

The exception mechanism looks at the site of the throw expression and asks, has this occurred within a try block? If it has, the catch clauses associated with the try block are examined to see whether there is a catch clause capable of handling the exception. If there is, the exception is handled and normal program execution begins again.

In our example, the throw expression does not occur within a try block. No attempt is made to handle the exception. The remaining statements of the function are not executed. The exception handling mechanism terminates check_integrity(). It resumes its search for a catch clause within the function that invoked check_integrity().

The question is asked again within the overloaded dereference operator: Has the call of check_integrity() occurred within a try block? No. The dereference operator terminates, and the exception mechanism resumes its search within the function that invoked the dereference operator. Has the call



*first 

occurred within a try block? In this case, the answer is yes. The associated catch clause is examined. The exception declaration matches the type of the exception object, and the body of the catch clause is executed. This completes the handling of the exception. Normal program execution resumes with the first statement following the catch clause:



// executed if element is not found 


// or if iterator_overflow exception is caught 


status = false; 


return status; 

What if the chain of function calls is unwound to main() and no appropriate catch clause is found? The language requires that every exception be handled. If no handler is found following examination of main(), the standard library terminate() function is invoked. By default, this terminates the program.

It is up to the programmer to decide how many statements within the function body to place within or outside the try block. If a statement can potentially result in an exception being thrown, not placing it within the try block guarantees that it is not handled within the function. That may or may not be OK. Not every function has to handle every potential exception.

For example, the dereference operator does not place the call of check_integrity() within a try block even though its invocation can result in an exception. Why? It's because the dereference operator is not prepared to handle the exception and can be safely terminated should the exception be thrown.

How do we know whether a function can safely ignore a potential thrown exception? Let's look again at the definition of the dereference operator:



inline int Triangular_iterator:: 


operator*() 


{ 


    check_integrity(); 


    return Triangular::_elems[ _index ]; 


} 

If check_integrity() fails, the value of _index must be invalid. The evaluation of the return statement is then certainly a bad idea. Should we add a try block to determine the result of invoking check_integrity()?

If check_integrity() had been implemented to return true or false, the definition of the dereference operator would need to guard against a false return value:



return check_integrity() 


       ? Triangular::_elems[ _index ] 


       : 0; 

The user, in turn, would need to guard against the dereference operator returning 0.

Because check_integrity() throws an exception, these guards are unnecessary. The return statement of the dereference operator is guaranteed to be executed only if no exception is thrown ?that is, when it is safe to evaluate the return statement. Otherwise, the function terminates before the statement is ever reached.

Why does the has_elem() function of the preceding section couch its dereference of first in a try block? It could simply allow the iterator_overflow exception to go up to its invoking function. Alternatively, why doesn't has_elem() worry about other potential exceptions? For example, it could add a catch-all to handle any exception thrown during its evaluation. The two decisions are flip sides of the same coin.

has_elem() provides a specific functionality: to say true or false to whether elem is present within the range of elements marked by first, last. To accomplish this, it iterates across the elements, incrementing first until either the element is found or each element has been examined. The dereference and increment of first are implementation details of has_elem(). The iterator_overflow exception is an aspect of that implementation, and I chose to localize it within has_elem() because has_elem() has the best knowledge of the significance of that exception within the executing program.

The function invoking has_elem() must know whether elem is present within the range marked off by first,last. Knowing that the range itself is invalid is probably important to the project, and that is why we log it. However, it is not something that the function invoking has_elem() is likely capable of handling. Therefore, I chose to shield it from the iterator_overflow exception.

On the flip side, the implementation of has_elem() is too focused on determining whether elem is present to be capable of handling all potential exceptions. For example, if the program's heap memory is exhausted, that is something too catastrophic for the implementer of has_elem() to take pains over.

When an exception occurs within the try block of a function that is not handled by the associated catch clauses, that function is terminated just as if there had not been a try block present. The search for a catch handler continues up the call chain to the invoking function. Within has_elem(), an iterator_overflow exception is handled. Are there any potential uncaught exceptions that might be thrown? Our examination of the call chain within has_elem() convinces us that there are not.

A common beginner mistake is to confuse C++ exceptions with hardware exceptions, such as a segmentation fault or bus error. For a C++ exception to be thrown, there is a throw expression somewhere in the program code that users are able to find.

    I l@ve RuBoard Previous Section Next Section