|
Exception-Safe Class Design, Part 1: Copy Assignment |
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 a strongly exception-safe Widget2::operator=() unless T1 provides suitable facilities through some other function (but if T1 can do that, why doesn't it for T1::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?
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 { Widget(); // initializes pimpl_ with new WidgetImpl ~Widget(); // must be provided, because the implicit // version causes usage problems (see GotW #62) // ... private: class WidgetImpl; auto_ptr<WidgetImpl> 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<WidgetImpl> 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 }
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?
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.
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 (nearly) 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<Widget> w_; // hold the unsafe-to-copy // Widget at arm's length public: void Swap( MyClass& other ) throw() { auto_ptr<Widget> temp( w_ ); w_ = other.w_; other.w_ = temp; }
MyClass& operator=( const MyClass& other ) { /* canonical form */ }
// ... destruction, copy construction, // and copy assignment ... };
To quote Scott Meyers:
"When I give talks on EH, I teach people two things:
"- 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.
"Then I tell them to 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.
Copyright © 2009 Herb Sutter |