This is the original GotW problem and solution substantially as posted to Usenet. See the book More Exceptional C++ (Addison-Wesley, 2002) for the most current solution to this GotW issue. The solutions in the book have been revised and expanded since their initial appearance in GotW. The book versions also incorporate corrections, new material, and conformance to the final ANSI/ISO C++ standard. |
Exception-Safe Function Calls Difficulty: 8 / 10Regular readers of GotW know that exception safety is anything but trivial. This puzzle points out an exception safety problem that was discovered only weeks before posting, and shows how best to avoid it in your own code. ProblemJG Question1. In each of the following statements, what can you say about the order of evaluation of the functions f, g, and h and the expressions expr1 and expr2? Assume that expr1 and expr2 do not contain more function calls. // Example 1(a)
//
f( expr1, expr2 ); // Example 1(b)
//
f( g( expr1 ), h( expr2 ) ); Guru Questions2. In your travels through the dusty corners of your company's code archives, you find the following code fragment: // Example 2
// // In some header file:
void f( T1*, T2* ); // In some implementation file:
f( new T1, new T2 ); Does this code have any potential exception safety problems? Explain. 3. As you continue to root through the archives, you see that someone must not have liked Example 2 because later versions of the files in question were changed as follows: // Example 3
// // In some header file:
void f( auto_ptr<T1>, auto_ptr<T2> ); // In some implementation file:
f( auto_ptr<T1>( new T1 ), auto_ptr<T2>( new T2 ) ); What improvements does this version offer over Example 2, if any? Do any exception safety problems remain? Explain. 4. Demonstrate how to write an auto_ptr_new facility that solves the safety problems in Question 3 and can be invoked as follows: // Example 4
// // In some header file (same as in Example 2b):
void f( auto_ptr<T1>, auto_ptr<T2> ); // In some implementation file:
f( auto_ptr_new<T1>(), auto_ptr_new<T2>() ); SolutionRecap: Evaluation Orders and Disorders1. In each of the following statements, what can you say about the order of evaluation of the functions f, g, and h and the expressions expr1 and expr2? Assume that expr1 and expr2 do not contain more function calls. Ignoring threads (which are beyond the scope of the C++ Standard), the answer to the first question hinges on these basic rules: 1. All of a function's arguments must be fully evaluated before the function is called. This includes the completion of any side effects of expressions used as function arguments. 2. Once the execution of a function begins, no expressions from the calling function begin, or continue, to be evaluated until execution of the called function has completed. Function executions never interleave with each other. 3. Expressions used as function arguments may generally be evaluated in any order, including interleaved, except as otherwise restricted by the other rules. Given those rules, let's see what happens in our opening examples: // Example 1(a)
//
f( expr1, expr2 ); All we can say is: Both expr1 and expr2 must be evaluated before f() is called. That's it. The compiler may choose to perform the evaluation of expr1 before, after, or interleaved with the evaluation of expr2. There are enough people who find this surprising that it comes up as a regular question on the newsgroups, but it's just a direct result of the C and C++ rules about sequence points. // Example 1(b)
//
f( g( expr1 ), h( expr2 ) ); The functions and expressions may be evaluated in any order that respects the following rules: - expr1 must be evaluated before g() is called. - expr2 must be evaluated before h() is called. - both g() and h() must complete before f() is called. - The evaluations of expr1 and expr2 may be interleaved with each other, but nothing may be interleaved with any of the function calls. For example, no part of the evaluation of expr2 nor the execution of h() may occur from the time g() begins until it ends. That's it. For example, this means that any one or more of the following are possible: - Either g() or h() could be called first. - Evaluation of expr1 could begin, then be interrupted by h() being called, then complete. (Likewise for expr2 and g().) Some Function Call Exception Safety Problems2. In your travels through the dusty corners of your company's code archives, you find the following code fragment: // Example 2
// // In some header file:
void f( T1*, T2* ); // In some implementation file:
f( new T1, new T2 ); Does this code have any potential exception safety problems? Explain. Yes, there are several potential exception safety problems. Brief recap: An expression like "new T1" is called, simply enough, a new-expression. Recall what a new-expression really does (I'll ignore placement and array forms for simplicity, since they're not very relevant here): - it allocates memory - it constructs a new object in that memory - if the construction fails because of an exception the allocated memory is freed
So each new-expression is essentially a series of two function calls: one call to operator new() (either the global one, or one provided by the type of the object being created), and then a call to the constructor. For Example 1, consider what happens if the compiler decides to generate code as follows: 1: allocate memory for T1 2: construct T1 3: allocate memory for T2 4: construct T2 5: call f() The problem is this: If either step 3 or step 4 fails because of an exception, the C++ standard does not require that the T1 object be destroyed and its memory deallocated. This is a classic memory leak, and clearly not a Good Thing. Another possible sequence of events is this: 1: allocate memory for T1 2: allocate memory for T2 3: construct T1 4: construct T2 5: call f() This sequence has, not one, but two different exception safety problems with different effects: a) If step 3 fails because of an exception, then the memory allocated for T1 is automatically deallocated (step 1 is undone), but the standard does not require that the memory allocated for the T2 object be deallocated. The memory is leaked. b) If step 4 fails because of an exception, then the T1 object has been allocated and fully constructed, but the standard does not require that it be destroyed and its memory deallocated. The T1 object is leaked. "Hmm," you might wonder, "then why does this exception safety loophole exist at all? Why doesn't the standard just prevent the problem by requiring compilers to Do The Right Thing when it comes to cleanup?" The basic answer is that it wasn't noticed, and even now that it has been noticed it might not be desirable to fix it. The C++ standard allows the compiler some latitude with the order of evaluation of expressions because this allows the compiler to perform optimizations that might not otherwise be possible. To permit this, the expression evaluation rules are specified in a way that is not exception-safe, and so if you want to write exception-safe code you need to know about, and avoid, these cases. (See below for how best to do this.) Remember that C++ exception safety in general has not been well understood until fairly recently (namely the summer of 1997, just months before the standard was completed). Even so, the committee managed to add exception safety guarantees to the standard library, albeit necessarily at the eleventh hour in the last two meetings before the standard was cast in stone; many of the guarantees were added literally days before the concrete set. The problem with new-expressions has now been noticed, but for the reasons above the committee will have to decide whether the hole should be "fixed" or whether it should continue to be allowed in the name of compiler optimization flexibility. 3. As you continue to root through the archives, you see that someone must not have liked Example 2 because later versions of the files in question were changed as follows: // Example 3
// // In some header file:
void f( auto_ptr<T1>, auto_ptr<T2> ); // In some implementation file:
f( auto_ptr<T1>( new T1 ), auto_ptr<T2>( new T2 ) ); What improvements does this version offer over Example 2, if any? Do any exception safety problems remain? Explain. This code attempts to "throw auto_ptr at the problem." Many people believe that a smart pointer like auto_ptr is an exception safety panacea. It is not. Nothing has changed. Example 3 is still not exception-safe, for exactly the same reasons as before. Specifically, the problem is that the resources are safe only if they make it into a managing auto_ptr, but the same problems already noted above can still occur here before either auto_ptr constructor is ever reached. This is because both of the two problematic execution orders mentioned above are still possible, just with the auto_ptr constructors tacked onto the end before f(). For one example: 1: allocate memory for T1 2: construct T1 3: allocate memory for T2 4: construct T2 5: construct auto_ptr<T1> 6: construct auto_ptr<T2> 7: call f() In the above case, the same problems as before are still present if either of steps 3 or 4 throws. Similarly: 1: allocate memory for T1 2: allocate memory for T2 3: construct T1 4: construct T2 5: construct auto_ptr<T1> 6: construct auto_ptr<T2> 7: call f() Again, the same problems as before are still present if either of steps 3 or 4 throws. Fortunately, though, this is not a problem with auto_ptr; auto_ptr is just being used the wrong way, that's all. In a moment, we'll see several ways to use it better. Aside: A Non-SolutionNote that the following is not a solution: // In some header file:
void f( auto_ptr<T1> = auto_ptr<T1>( new T1 ),
auto_ptr<T2> = auto_ptr<T1>( new T2 ) ); // In some implementation file:
f(); Why is this code not a solution? Because it's identical to Example 3 in terms of expression evaluation. Default arguments are considered to be created in the function call expression, even though they're actually written somewhere else entirely (namely, in the function declaration). A Limited Solution4. Demonstrate how to write an auto_ptr_new facility that solves the safety problems in Question 3 and can be invoked as follows: // Example 4
// // In some header file (same as in Example 2b):
void f( auto_ptr<T1>, auto_ptr<T2> ); // In some implementation file:
f( auto_ptr_new<T1>(), auto_ptr_new<T2>() ); The simplest solution is to provide a function template like the following: // Example 4(a): Partial solution
//
template<class T>
auto_ptr<T> auto_ptr_new()
{
return auto_ptr<T>( new T );
} This solves the exception safety problems with Examples 2 through 4. No sequence of generated code can cause resources to be leaked, because now all we have is two functions, and we know that one must be executed entirely before the other. Consider the following evaluation order: 1: call auto_ptr_new<T1>() 2: call auto_ptr_new<T2>() If step 1 throws, there are no leaks because the auto_ptr_new() template is itself strongly exception-safe. If step 2 throws, then is the temporary auto_ptr<T1> created by step 1 guaranteed to be cleaned up? The answer is: Yes, it is. Now, one might wonder, isn't this pretty much the same as the "new T1" object being created in the corresponding case in Example 2, which isn't correctly cleaned up? No, this time it's not quite the same, because here the auto_ptr<T1> is actually a temporary object, and cleanup of temporary objects is correctly specified in the Standard. From the Standard, in 12.2/3: Temporary objects are destroyed as the last step in evaluating the full-expression (_intro.execution_) that (lexically) contains the point where they were created. This is true even if that evaluation ends in throwing an exception.
But Example 4(a) is only a limited solution: It only works with a default constructor, which breaks if a given type T doesn't have a default constructor, or if you don't want to use it. A more general solution is still needed. Generalizing the auto_ptr_new() SolutionAs pointed out by Dave Abrahams, we can extend the solution to support non-default constructors by providing a family of overloaded function templates: // Example 4(b): Improved solution
//
template<class T>
auto_ptr<T> auto_ptr_new()
{
return auto_ptr<T>( new T );
} template<class T, class Arg1>
auto_ptr<T> auto_ptr_new( const Arg1& arg1 )
{
return auto_ptr<T>( new T( arg1 ) );
} template<class T, class Arg1, class Arg2>
auto_ptr<T> auto_ptr_new( const Arg1& arg1,
const Arg2& arg2 )
{
return auto_ptr<T>( new T( arg1, arg2 ) );
} // etc. Now auto_ptr_new() fully and naturally supports non-default construction. A Better SolutionAlthough auto_ptr_new() is nice, is there any way we could have avoided all of the exception safety problems without writing such helper functions? Could we have avoided the problems with better coding standards? The answer is yes, and here is one possible standard that would have eliminated the problem: POSSIBLE GUIDELINE (Alternative #1): Never allocate resources (e.g., via new) in the same expression as any other code that could throw an exception. This applies even if the new resource will immediately be managed (e.g., passed to an auto_ptr constructor) in the same expression.
In the Example 3 code, the way to satisfy this guideline is to move one of the temporary auto_ptrs into a separate named variable: // Example 3(a): A solution
// // In some header file:
void f( auto_ptr<T1>, auto_ptr<T2> ); // In some implementation file:
{
auto_ptr<T1> t1( new T1 );
f( t1, auto_ptr<T2>( new T2 ) );
} This satisfies Guideline #1 because, although we are still allocating a resource, it can't be leaked due to an exception because it's not being created in the same expression as any other code that could throw.[1] Here is another possible coding standard, which is even simpler and easier to get right (and easier to catch in code reviews): POSSIBLE GUIDELINE (Alternative #2): Perform every resource allocation (e.g., new) in its own code statement which immediately gives the new resource to a manager object (e.g., auto_ptr).
In Example 3, the way to satisfy this guideline is to move both of the temporary auto_ptrs into separate named variables: // Example 3(b): A simpler solution
// // In some header file:
void f( auto_ptr<T1>, auto_ptr<T2> ); // In some implementation file:
{
auto_ptr<T1> t1( new T1 );
auto_ptr<T2> t2( new T2 );
f( t1, t2 );
} This satisfies Guideline #2, and it required a lot less thought to get it right. Each new resource is created in its own expression and is immediately given to a managing object. SummaryMy recommendation is: GUIDELINE: Perform every resource allocation (e.g., new) in its own code statement which immediately gives the new resource to a manager object (e.g., auto_ptr).
This guideline is easy to understand and remember, it neatly avoids all of the exception safety problems in the original problem, and by mandating the use of manager objects it helps to avoid many other exception safety problems as well. This guideline is a good candidate for inclusion in your team's coding standards. AcknowledgmentsThis GotW was prompted by a discussion thread on comp.lang.c++.moderated. This solution draws on observations presented by gurus James Kanze, Steve Clamage, and Dave Abrahams in that and other threads, and in private correspondence. Notes1. Yes, I'm being a little fuzzy, because I know that the body of f() is included in the expression evaluation and we don't care whether or not it throws. |