TITLE: exception safety in function calls (Newsgroups: comp.lang.c++.moderated, 26 June 99) AUTHOR: Herb Sutter ------------------------------------------------------------------- Guru of the Week problems and solutions are posted regularly on news:comp.lang.c++.moderated. For past problems and solutions see the GotW archive at www.peerdirect.com/resources. (c) 1999 H P Sutter. News archives may keep copies of this article. ------------------------------------------------------------------- _______________________________________________________ GotW #56: Exception-Safe Function Calls (originally proposed by Dave Abrahams) Difficulty: 8 / 10 _______________________________________________________ Congratulations to John Potter, the Guru of the Week! John was the first to post what I recommend as the most reasonable solution to the problem described by this issue of GotW. Acknowledgments --------------- This GotW was prompted by a discussion thread on comp.lang.c++.moderated. The following solution draws on observations presented by gurus James Kanze, Steve Clamage, and Dave Abrahams in that and other threads, and in private correspondence. Recap: Evaluation Orders and Disorders -------------------------------------- For the first question, I'll add the (originally unstated) assumption that expr1 and expr2 do not contain function calls. >JG Question >----------- > >1. 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 Problems -------------------------------------------- >2. 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 no one noticed the problem. Remember that C++ exception safety in general has not been well understood until fairly recently (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 just noticed, and will likely be addressed in a Technical Corrigendum to the standard. >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, auto_ptr ); > > // In some implementation file: > f( auto_ptr( new T1 ), auto_ptr( 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. What's amusing here isn't just that adding auto_ptr didn't solve anything, but that the auto_ptrs themselves can now be leaked. This is like sending in troops to contain a riot, only to find out that not only is the riot undiminished, but the troops themselves got swept away in it and clobbered. Wasteful, that. To illustrate this effect, consider the following possible execution order: 1: allocate memory for T1 2: construct T1 3: construct auto_ptr 4: allocate memory for T2 5: construct T2 6: construct auto_ptr 7: call f() A cynical person would probably consider this funny. Now, if either step 4 or step 5 fails, both the T1 object AND the auto_ptr object are never cleaned up. Not only is the auto_ptr unable to protect the T1 resource (even though it has successfully assumed ownership), but it can't even protect itself. 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-Solution --------------------- Note that the following is not a solution: // Example 4 // // In some header file: void f( auto_ptr = auto_ptr( new T1 ), auto_ptr = auto_ptr( new T1 ) ); // 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 Solution ------------------ >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 5 > // > > // In some header file (same as in Example 2b): > void f( auto_ptr, auto_ptr ); > > // In some implementation file: > f( auto_ptr_new(), auto_ptr_new() ); The simplest solution is to provide a function template like the following: // Example 5(a): Partial solution // template auto_ptr auto_ptr_new() { return auto_ptr( 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() 2: call auto_ptr_new() 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 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 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 5(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() Solution ---------------------------------------- As pointed out by Dave Abrahams, we can extend the solution to support non-default constructors by providing a family of overloaded function templates: // Example 5(b): Improved solution // template auto_ptr auto_ptr_new() { return auto_ptr( new T ); } template auto_ptr auto_ptr_new( const Arg1& arg1 ) { return auto_ptr( new T( arg1 ) ); } template auto_ptr auto_ptr_new( const Arg1& arg1, const Arg2& arg2 ) { return auto_ptr( new T( arg1, arg2 ) ); } // etc. Now auto_ptr_new() fully and naturally supports non-default construction. A Better Solution ----------------- Although 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, auto_ptr ); // In some implementation file: { auto_ptr t1( new T1 ); f( t1, auto_ptr( 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.[*] [*] 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. 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, auto_ptr ); // In some implementation file: { auto_ptr t1( new T1 ); auto_ptr 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. SUMMARY ------- My 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.