TITLE: exception-safe class design (Newsgroups: comp.lang.c++.moderated, 8 Aug 99) AUTHOR: hsutter@peerdirect.com (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 #59: Exception-Safe Class Design Part I: Assignment Difficulty: 7 / 10 _______________________________________________________ Review: Exception Safety Canonical Forms ---------------------------------------- >1. What are the canonical three levels of exception > safety? Briefly explain each one and why it is > important. The canonical Abrahams Guarantees are as follows. 1. Basic Guarantee: If an exception is thrown, no resources are leaked, and objects remain in a destructible and usable -- but not necessarily predictable -- state. This is the weakest usable level of exception safety, and is appropriate where client code can cope with failed operations that have already made changes to objects' state. 2. Strong Guarantee: If an exception is thrown, program state remains unchanged. This level always implies global commit-or-rollback semantics, including that no references or iterators into a container be invalidated if an operation fails. In addition, certain functions must provide an even stricter guarantee in order to make the above exception safety levels possible: 3. Nothrow Guarantee: The function will not emit an exception under any circumstances. It turns out that you can't implement the basic or strong guarantee unless certain functions -- specifically, destructors, deallocation functions, and Swap() -- are guaranteed not to throw. As we will see below, an important feature of the standard auto_ptr is that no auto_ptr operation will throw. >2. What is the canonical form of strongly > exception-safe copy assignment? It involves two steps: First, provide a nonthrowing Swap() function that swaps the guts (state) of two objects: void T::Swap( T& other ) throw() { // ...swap the guts of *this and other... } Second, implement operator=() using the "create a temporary and swap" idiom: T& T::operator=( const T& other ) { T temp( other ); // do all the work off to the side Swap( temp ); // then "commit" the work using return *this; // nonthrowing operations only } The Cargill Widget Example -------------------------- This brings us to the Guru questions, starting with a new exception safety challenge proposed by Tom Cargill: >3. Consider the following class: > > // Example 3: The Cargill Widget Example > // > class Widget > { > // ... > private: > T1 t1_; > T2 t2_; > }; > > Assume that any T1 or T2 operation might throw. > Without changing the structure of the class, is it > possible to write a strongly exception-safe > Widget::operator=( const Widget& )? Why or why not? > Draw conclusions. Short answer: In general, no, it can't be done without changing the structure of Widget. To understand why, consider the following axiom: AXIOM: It is possible to write a strongly exception- safe T::operator=() if and only if it is possible to write a nonthrowing T::Swap(). In the Example 3 case, it's not possible to write a nonthrowing Widget::Swap() because there's no way that we can change the state of both of the t1_ and t2_ members atomically. Say that we attempt to change t1_, then attempt to change t2_. The problem is twofold: 1. If the attempt to change t1_ throws, t1_ must be unchanged. That is, to make Widget::operator=() strongly exception-safe relies fundamentally on the exception safety guarantees provided by T1, namely that T1::operator=() (or whatever mutating function we are using) either succeeds or does not change its target. This comes close to requiring the strong guarantee of T1::operator=(). (The same reasoning applies to T2::operator=().) 2. If the attempt to change t1_ succeeds, but the attempt to change t2_ throws, we've entered a "halfway" state and cannot in general roll back the change already made to t1_. Therefore, the way Widget is currently structured, its operator=() cannot be made strongly exception-safe. | Note also that Cargill's Widget Example isn't all | that different from the following simpler case: | | class Widget2 | { | // ... | private: | T1 t1_; | }; | | In the above code, problem #1 above still exists. | If T1::operator=() can throw in such a way that it | has already started to modify the target, there is | no way to write an atomic nonthrowing Swap(), and | hence no way to write a strongly exception-safe | Widget2::operator=(). Our goal: To write a Widget::operator=() that is strongly exception-safe, without making any assumptions about the exception safety of any T1 or T2 operation. Can it be done? Or is all lost? A Complete Solution: Using the Pimpl Idiom ------------------------------------------ The good news is that, even though Widget::operator=() can't be made strongly exception-safe without changing Widget's structure, the following simple transformation always works: >4. Describe and demonstrate a simple transformation > that works on any class in order to make strongly > exception-safe copy assignment possible and easy for > that class. Where have we seen this transformation > technique before in other contexts? Cite GotW issue > number(s). The way to solve the problem is hold the member objects by pointer instead of by value, preferably all behind a single pointer with a Pimpl transformation (described in GotW issues like 7, 15, 24, 25, and 28). Here is the canonical Pimpl exception-safety transformation: // Example 4: The canonical solution to // Cargill's Widget Example // class Widget { // ... private: class WidgetImpl; auto_ptr pimpl_; // ... provide copy construction and assignment // that work correctly, or suppress them ... }; class Widget::WidgetImpl { public: // ... T1 t1_; T2 t2_; }; Now we can easily implement a nonthrowing Swap(), which means we can easily implement exception-safe copy assignment: First, provide the nonthrowing Swap() function that swaps the guts (state) of two objects (note that this function can provide the nothrow guarantee because no auto_ptr operations are permitted to throw exceptions): void Widget::Swap( Widget& other ) throw() { auto_ptr temp( pimpl_ ); pimpl_ = other.pimpl_; other.pimpl_ = temp; } Second, implement the canonical form of operator=() using the "create a temporary and swap" idiom: Widget& Widget::operator=( const Widget& other ) { Widget temp( other ); // do all the work off to the side Swap( temp ); // then "commit" the work using return *this; // nonthrowing operations only } A Potential Objection, and Why It's Unreasonable ------------------------------------------------ Some may object: "Aha! Therefore this proves exception safety is unattainable in general, because you can't solve the general problem of making any arbitrary class strongly exception-safe without changing the class!" Such a position is unreasonable and untenable. The Pimpl transformation, a minor structural change, IS the solution to the general problem. To say, "no, you can't do that, you have to be able to make an arbitrary class exception-safe without any changes," is unreasonable for the same reason that "you have to be able to make an arbitrary class meet New Requirement #47 without any changes" is unreasonable. For example: Unreasonable Statement #1: "Polymorphism doesn't work in C++ because you can't make an arbitrary class usable in place of a Base& without changing it (to derive from Base)." Unreasonable Statement #2: "STL containers don't work in C++ because you can't make an arbitrary class usable in an STL container without changing it (to provide an assignment operator)." Unreasonable Statement #3: "Exception safety doesn't work in C++ because you can't make an arbitrary class strongly exception-safe without changing it (to put the internals in a Pimpl class)." Clearly all the above arguments are equally bankrupt, and the Pimpl transformation is indeed the general solution to strongly exception-safe objects. So, what have we learned? Conclusion 1: Exception Safety Affects a Class's Design ------------------------------------------------------- Exception safety is never "just an implementation detail." The Pimpl transformation is a minor structural change, but still a change. GotW #8 shows another example of how exception safety considerations can affect the design of a class's member functions. Conclusion 2: You Can Always Make Your Code Strongly Exception-Safe ------------------------------------------- There's an important principle here: Just because a class you use isn't in the least exception-safe is no reason that YOUR code that uses it can't be strongly exception-safe. Anybody can use a class that lacks a strongly exception-safe copy assignment operator and make that use exception-safe. The "hide the details behind a pointer" technique can be done equally well by either the Widget implementor or the Widget user... it's just that if it's done by the implementor it's always safe, and the user won't have to do this: class MyClass { auto_ptr w_; // hold the unsafe-to-copy // Widget at arm's length public: void Swap( MyClass& other ) throw() { auto_ptr temp( w_ ); w_ = other.w_; other.w_ = temp; } MyClass& operator=( const MyClass& other ) { /* canonical form */ } // ... copy construction and assignment ... }; Conclusion 3: Use Pointers Judiciously -------------------------------------- To quote Scott Meyers: > - POINTERS ARE YOUR ENEMIES, because they lead to > the kinds of problems that auto_ptr is designed to > eliminate. To wit, bald pointers should normally be owned by manager objects that own the pointed-at resource and perform automatic cleanup. Then Scott continues: > - POINTERS ARE YOUR FRIENDS, because operations on > pointers can't throw. >have a nice day :-) Scott captures a fundamental dichotomy well. Fortunately, in practice you can and should get the best of both worlds: - USE POINTERS BECAUSE THEY ARE YOUR FRIENDS, because operations on pointers can't throw. - KEEP THEM FRIENDLY BY WRAPPING THEM IN MANAGER OBJECTS like auto_ptrs, because this guarantees cleanup. This doesn't compromise the nonthrowing advantages of pointers because auto_ptr operations never throw either (and you can always get at the real pointer inside an auto_ptr whenever you need to). Indeed, often the best way to implement the Pimpl idiom is exactly as shown in Example 4 above, by using a pointer (in order to take advantage of nonthrowing operations) while still wrapping the dynamic resource safely in an auto_ptr manager object. Just remember that now your object must provide its own copy construction and assignment with the right semantics for the auto_ptr member, or disable them if copy construction and assignment don't make sense for the class.