I l@ve RuBoard Previous Section Next Section

Solution

graphics/bulb_icon.gif

First, let's think about the implications of the given assumptions:

  1. Different orders of evaluating function parameters are ignored, and exceptions thrown by destructors are ignored.[15] Follow-up question for the intrepid: How many more execution paths are there if destructors are allowed to throw?

    [15] Never allow an exception to propagate from a destructor. Code that does this can't be made to work well. See Item 16 for more about "destructors that throw and why they're evil."

  2. Called functions are considered atomic. For example, the call "e.Title()" could throw for several reasons (for example, it could throw an exception itself, it could fail to catch an exception thrown by another function it has called, or it could return by value and the temporary object's constructor could throw). All that matters to the function is whether performing the operation e.Title() results in an exception being thrown or not.

  3. To count as a different execution path, an execution path must be made up of a unique sequence of function calls performed and exited in the same way.

So, how many possible execution paths are there? Answer: 23 (in just three lines of code!).

If you found: Rate yourself:
3 Average
4?4 Exception-aware
15?3 Guru material

The 23 are made up of:

  • 3 nonexceptional code paths

  • 20 invisible exceptional code paths

By nonexceptional code paths I mean paths of execution that happen even if there are no exceptions thrown. Nonexceptional code paths result from normal C++ program flow. On the other hand, by exceptional code paths I mean those paths of execution that arise as a result of an exception being thrown or propagated, and I'll consider those paths separately.

Nonexceptional Code Paths

For the nonexceptional execution paths, the trick was to know C/C++'s short-circuit evaluation rule.



if( e.Title() == "CEO" || e.Salary() > 100000 ) 


  1. If e.Title() == "CEO" evaluates to true, then the second part of the condition doesn't need to be evaluated (for example, e.Salary() will never be called), but the cout will be performed.

    With suitable overloads for ==, ||, and/or > in the if's condition, the || could actually turn out to be a function call. If it is a function call, the short-circuit evaluation rule would be suppressed and both parts of the condition would be evaluated all the time.

  2. If e.Title() != "CEO" but e.Salary() > 100000, both parts of the condition will be evaluated and the cout will be performed.

  3. If e.Title() != "CEO" and e.Salary() <= 100000, the cout will not be performed.

Exceptional Code Paths

This leaves the exceptional execution paths.



String EvaluateSalaryAndReturnName( Employee e ) 


  ^*^                                   ^4^


  1. The argument is passed by value, which invokes the Employee copy constructor. This copy operation might throw an exception.

    *. String's copy constructor might throw while copying the temporary return value into the caller's area. We'll ignore this one, however, because it happens outside this function (and it turns out that we have enough execution paths of our own to keep us busy anyway).

    
    
    if( e.Title() == "CEO" || e.Salary()  >  100000 ) 
    
    
           ^5^   ^7^  ^6^ ^11^    ^8^   ^10^   ^9^
    
    
    
  2. The Title() member function might itself throw, or it might return an object of class type by value, and that copy operation might throw.

  3. To match a valid operator==(), the string literal may need to be converted to a temporary object of class type (probably the same as e.Title()'s return type), and that construction of the temporary might throw.

  4. If operator==() is a programmer-supplied function, it might throw.

  5. Similar to #5, Salary() might itself throw, or it might return a temporary object and this construction operation might throw.

  6. Similar to #6, a temporary object may need to be constructed and this construction might throw.

  7. Similar to #7, this might be a programmer-provided function and therefore might throw.

  8. Similar to #7 and #10, this might be a programmer-provided function and therefore might throw.

    
    
    cout << e.First() << " " << e.Last() << " is overpaid" << endl; 
    
    
         ^12^ ^17^   ^13^   ^14^  ^18^  ^15^              ^16^
    
    
    
  9. As documented in the C++ Standard, any of the five calls to operator<< might throw.

  10. Similar to #5, First() and/or Last() might throw, or each might return a temporary object and those construction operations might throw.

    
    
    return e.First()   +   " "   +   e.Last(); 
    
    
             ^19^    ^22^ ^21^ ^23^   ^20^
    
    
    
  11. Similar to #5, First() and/or Last() might throw, or each might return a temporary object and those construction operations might throw.

  12. Similar to #6, a temporary object may need to be constructed and this construction might throw.

  13. Similar to #7, this might be a programmer-provided function and therefore might throw.

Guideline

graphics/guideline_icon.gif

Always be exception-aware. Know what code might emit exceptions.


One purpose of this Item was to demonstrate just how many invisible execution paths can exist in simple code in a language that allows exceptions. Does this invisible complexity affect the function's reliability and testability? See the following Item for the answer.

    I l@ve RuBoard Previous Section Next Section