I l@ve RuBoard Previous Section Next Section

Solution

graphics/bulb_icon.gif

First, before we get into the solution proper, a word about assumptions.

As the problem stated, we will assume that all called functions梚ncluding the stream functions梐re strongly exception-safe (might throw but do not have side effects), and that any objects being used, including temporaries, are exception-safe (clean up their resources when destroyed).

Streams happen to throw a monkey wrench into this because of their possible "un-rollbackable" side effects. For example, operator<< might throw after emitting part of a string that can't be "un-emitted"; also, the stream's error state may be set. We will ignore those issues for the most part; the point of this discussion is to examine how to make a function exception-safe when the function is specified to have two distinct side effects.

So here's the question again: Is the function from Item 18 exception-safe (works properly in the presence of exceptions) and exception-neutral (propagates all exceptions to the caller)?



String EvaluateSalaryAndReturnName( Employee e ) 


{


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


  {


    cout << e.First() << " " << e.Last() << " is overpaid" << endl;


  }


  return e.First() + " " + e.Last();


}


As written, this function satisfies the basic guarantee: In the presence of exceptions, the function does not leak resources.

This function does not satisfy the strong guarantee. The strong guarantee says that if the function fails due to an exception, program state must not be changed. EvaluateSalaryAndReturnName(), however, has two distinct side effects (as hinted at in the function's name).

  • An "...overpaid..." message is emitted to cout.

  • A name string is returned.

As far as the second side effect is concerned, the function already meets the strong guarantee, because if an exception occurs the value will never be returned. As far as the first side effect is concerned, though, the function is not exception-safe for two reasons:

  • If an exception is thrown after the first part of the message has been emitted to cout but before the message has been completed (for example, if the fourth operator<< throws), then a partial message was emitted to cout.[16]

    [16] If you're thinking that it's a little pedantic to worry about whether a message is completely cout'ed or not, you're partly right. In this case, maybe no one would care. However, the same principle applies to any function that attempts to perform two side effects, and that's why the following discussion is useful.

  • If the message was emitted successfully but an exception occurs later in the function (for example, during the assembly of the return value), then a message was emitted to cout even though the function failed because of an exception.

Finally, the function clearly does not satisfy the nothrow guarantee: Lots of operations might throw, and there's no try/catch block or throw() specification in sight.

Guideline

graphics/guideline_icon.gif

Understand the basic, strong, and nothrow exception-safety guarantees.


To meet the strong guarantee, either both effects are completed or an exception is thrown and neither effect is performed.

Can we accomplish this? Here's one way we might try it:



// Attempt #1: An improvement? 


//


String EvaluateSalaryAndReturnName( Employee e )


{


  String result = e.First() + " " + e.Last();


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


  {


    String message = result + " is overpaid\n";


    cout << message;


  }





  return result;


}


This isn't bad. Note that we've replaced the endl with a newline character (which isn't exactly equivalent) in order to get the entire string into one operator<< call. (Of course, this doesn't guarantee that the underlying stream system won't itself fail partway through writing the message, resulting in incomplete output, but at least we've done the best we can do at this high level.)

We still have one minor quibble, however, as illustrated by the following client code:



// A problem... 


//


String theName;


theName = EvaluateSalaryAndReturnName( bob );


The String copy constructor is invoked because the result is returned by value, and the copy assignment operator is invoked to copy the result into theName. If either copy fails, then the function has completed its side effects (since the message was completely emitted and the return value was completely constructed), but the result has been irretrievably lost (oops).

Can we do better, and perhaps avoid the problem by avoiding the copy? For example, we could let the function take a non-const String reference parameter and place the return value in that.



// Attempt #2: Better now? 


//


void EvaluateSalaryAndReturnName( Employee e,


                                  String&  r )


{


  String result = e.First() + " " + e.Last();


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


  {


    String message = result + " is overpaid\n";


    cout << message;


  }





  r = result;


}


This may look better, but it isn't, because the assignment to r might still fail, which leaves us with one side effect complete and the other incomplete. Bottom line, this attempt doesn't really buy us much.

One way to solve the problem is to return a pointer to a dynamically allocated String. But the best solution is to go a step farther and return the pointer in an auto_ptr.



// Attempt #3: Correct (finally!). 


//


auto_ptr<String>


EvaluateSalaryAndReturnName( Employee e )


{


  auto_ptr<String> result


      = new String( e.First() + " " + e.Last() );





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


  {


    String message = (*result) + " is overpaid\n";


    cout << message;


  }





  return result;  // rely on transfer of ownership;


                  // this can't throw


}


This does the trick, because we have effectively hidden all the work to construct the second side effect (the return value), while we ensured that it can be safely returned to the caller using only nonthrowing operations after the first side effect has completed (the printing of the message). We know that once the function is complete, the returned value will make it successfully into the hands of the caller and be correctly cleaned up in all cases. If the caller accepts the returned value, the act of accepting a copy of the auto_ptr causes the caller to take ownership; and if the caller does not accept the returned value, say by ignoring the return value, the allocated String will automatically be cleaned up as the temporary auto_ptr holding it is destroyed. The price for this extra safety? As often happens when implementing strong exception safety, the strong safety comes at the (usually minor) cost of some efficiency梙ere, the extra dynamic memory allocation. But, when it comes to trading off efficiency for predictability and correctness, we ought to prefer the latter two.

Let's discuss, for a moment, exception safety and multiple side effects. In this case, it turned out to be possible in Attempt #3 to perform both side effects with essentially commit-or-rollback semantics (except for the stream issues). It was possible because there turned out to be a technique by which the two effects could be performed atomically梩hat is, all the "real" preparatory work for both could be completed in such a way that actually performing the visible side effects could be done using only nonthrowing operations.

Even though we were lucky this time, it's not always that simple. It's impossible to write strongly exception-safe functions that have two or more unrelated side effects that cannot be performed atomically (for example, what if the two side effects here had been to emit one message to cout and another to cerr?), since the strong guarantee states that in the presence of exceptions "program state will remain unchanged"梚n other words, if there's an exception, there must be no side effects. When you come across a case in which the two side effects cannot be made to work atomically, usually the only way to get strong exception safety is to split the one function into two others that can be performed atomically. That way, at least, the fact that they can't be done atomically is visible to the calling code.

Guideline

graphics/guideline_icon.gif

Prefer cohesion. Always endeavor to give each piece of code梕ach module, each class, each function梐 single, well-defined responsibility.


In summary, this Item illustrates three important points.

  1. Providing the strong exception-safety guarantee often (but not always) requires you to trade off performance.

  2. If a function has multiple unrelated side effects, it cannot always be made strongly exception-safe. If not, it can be done only by splitting the function into several functions, each of whose side effects can be performed atomically.

  3. Not all functions need to be strongly exception-safe. Both the original code and Attempt #1 satisfy the basic guarantee. For many clients, Attempt #1 is sufficient and minimizes the opportunity for side effects to occur in the exceptional situations, without requiring the performance tradeoffs of Attempt #3.

There's a postscript to this solution regarding streams and side effects.

In this Item's problem statement, I said in part: Assume that all called functions are exception-safe (might throw but do not have side effects if they do throw), and that any objects being used, including temporaries, are exception-safe (clean up their resources when destroyed).

As it turns out, our assumption that no called function has side effects cannot be entirely true. In particular, there is no way to guarantee that the stream operations will not fail after partly emitting a result. This means that we can't get true commit-or-rollback fidelity from any function that performs stream output, at least not on these standard streams.

Another issue is that if the stream output fails, the stream state will have changed. We currently do not check for that or recover from it, but it is possible to further refine the function to catch stream exceptions and reset cout's error flags before rethrowing the exception to the caller.

    I l@ve RuBoard Previous Section Next Section