Solution

I l @ ve RuBoard

graphics/bulb.gif

This Item answers the following questions:

  • Can any arbitrary class be made exception-safe ”that is, without modifying its structure?

  • If not (that is, if exception safety does affect a class's design), is there any simple change that always works to let us make any arbitrary class exception-safe?

  • Are there exception safety consequences to the way we choose to express relationships among classes? Specifically, does it matter whether we choose to express a relationship using inheritance or using delegation?

Review: Exception Safety Canonical Forms

1. What are the three common levels of exception safety? Briefly explain each one and why it is important.

The canonical Abrahams Guarantees are as follows .

  • 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 when calling code can cope with failed operations that have already made changes to objects' states.

  • Strong guarantee: If an exception is thrown, program state remains unchanged. This level always implies 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:

  • Nothrow guarantee: The function will not emit an exception under any circumstances. It turns out that it is sometimes impossible to implement the strong or even the basic guarantee unless certain functions are guaranteed not to throw (for example, destructors, deallocation functions). 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?

The canonical form for copy assignment involves two steps. First, provide a nonthrowing Swap() function that swaps the guts, or internal 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 } 

Analyzing the Cargill Widget Example

This brings us to an exception safety challenge proposed by Tom Cargill:

3. Consider the following class:

  // Example 22-1: The Cargill Widget Example   //   class Widget   {   public:   Widget& operator=( const 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 .

In short: Exception safety cannot be achieved in general without changing the structure of Widget . In Example 22-1, it's not possible to write a safe Widget::operator=() at all. We cannot guarantee the Widget object will even be in a consistent final state if an exception is thrown, because there's no way we can change the state of both of the t1_ and t2_ members atomically, or even reliably back out to a consistent (same or different) state if things go wrong partway through. Say our Widget::operator=() attempts to change t1_ , then attempts to change t2_ (one or the other member has to be done first, it doesn't really matter which, in this case). The problem is twofold:

If the attempt to change t1_ throws, t1_ must be unchanged. That is, to make Widget::operator=() 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=() .

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_ . For example, what if our attempt to reassign t1_ 's original value, or any other reasonable value, also fails? Then the Widget object can't even guarantee recovery to a consistent state that maintains Widget 's invariants.

Therefore, the way Widget is structured in Example 22-1, its operator=() cannot be made strongly exception-safe. (See the accompanying sidebar, "A Simpler but Still Difficult Widget," for a simpler example that has a subset of the above problems.)

Our goal is 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 Simpler But Still Difficult Widget

Note that Cargill's Widget Example isn't all that different from the following simpler case.

 class Widget2 {   // ... private:   T1 t1_; }; 

Even for the simplified Widget2 , problem #1 in the main text 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 do so for T1::operator=() ?

A General Technique: Using the Pimpl Idiom

4. Describe and demonstrate a simple transformation that works on any class in order to make (nearly) strongly exception-safe copy assignment possible and easy for that class. Where have we seen this transformation technique before in other contexts?

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 to enable an almost strongly exception-safe assignment. Hold the member objects by pointer instead of by value, preferably all behind a single pointer with a Pimpl transformation. (For more details, including an analysis of the costs of using Pimpls and how to minimize those costs, see Exceptional C++ [Sutter00] Items 26 to 30.)

Example 22-2 illustrates the general exception safety-promoting transformation (alternatively, the pimpl_ could be held as a bald pointer or you can use some other pointer-managing object):

 // Example 22-2: The general solution to // Cargill's Widget Example // class Widget { public:   Widget();  // initializes pimpl_ with new WidgetImpl   ~Widget(); // must be provided, because the implicit        //  version causes usage problems        //  (see Items 30 and 31)   Widget& operator=( const Widget& );   // ...   private:     class WidgetImpl;     auto_ptr<WidgetImpl> pimpl_;     // ... provide copy construction that     //     works correctly, or suppress it ... }; // Then, typically in a separate // implementation file: // class Widget::WidgetImpl { public:   // ...   T1 t1_;   T2 t2_; }; 

Aside: Note that if you use an auto_ptr member, then: (a) you must either provide the definition of WidgetImpl with the definition of Widget , or if you want to keep hiding WidgetImpl you must write your own destructor for Widget even if it's a trivial destructor; [11] and (b) you should also provide your own copy construction and assignment for Widget , because you usually don't want transfer-of-ownership semantics for class members. If you have a different kind of smart pointer available, consider using that instead of auto_ptr , but the principles described here remain important.

[11] If you use the automatically compiler-generated destructor, that destructor will be defined in every translation unit, and therefore the definition of WidgetImpl must be visible in every translation unit.

Now we can easily implement a nonthrowing Swap() , which means we can easily implement exception-safe copy assignment that nearly meets the strong guarantee. First, provide the nonthrowing Swap() function that swaps the guts (state) of two objects. Note that this function can provide the no-throw guarantee that no exceptions will be thrown under any circumstances, because no auto_ptr operation is permitted to throw exceptions: [12]

[12] Note that replacing the three-line body of Swap() with the single line " swap( pimpl_, other.pimpl_ ); " is not guaranteed to work correctly, because std::swap() will not necessarily work correctly for auto_ptr s.

 void Widget::Swap( Widget& other ) /* throw() */ {   auto_ptr<WidgetImpl> temp( pimpl_ );   pimpl_ = other.pimpl_;   other.pimpl_ = temp; } 

Second, implement the common exception-safe 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 } 

This is nearly strongly exception-safe. It doesn't quite guarantee that if an exception is thrown program state will remain entirely unchanged. Do you see why? It's because, when we create the temporary Widget object and therefore its pimpl_ 's t1_ and t2_ members, the creation of those members (and/or their destruction if we fail) may cause side effects, such as changing a global variable, and there's no way we can know about or control that.

A Potential Objection, and Why It's Unreasonable

Some may leap upon this with the ardent battle cry: "Aha, so 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." I raise this point because some people have indeed raised this objection.

Such a conclusion seems unreasonable. The Pimpl transformation, a minor structural change, is indeed the solution to the general problem. Like most implementation goals, exception safety affects a class's design, period. Just as one wouldn't expect to make a class work polymorphically without accepting the slight change to inherit from the necessary base class, one wouldn't expect to make a class work in an exception-safe way without accepting the slight change to hold its members at arm's length. To illustrate , consider three statements:

  • 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 exception-safe without changing it (to put the internals in a Pimpl class).

The above arguments are equally fruitless, and the Pimpl transformation is indeed the general solution to writing classes that give useful exception safety guarantees (indeed, nearly the strong guarantee) without requiring any knowledge of the safety of class data members.

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 straightforward structural change, but still a change.

Conclusion 2: You Can Always Make Your Code (Nearly) 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 why code that uses it can't be strongly exception-safe (except for side effects) .

Anybody can use a class that lacks a strongly exception-safe copy assignment operator and make that use strongly exception-safe, except that of course if Widget operations cause side effects (such as changing a global variable), there's no way we can know about or control it. In other words, we can achieve what might be called the local strong guarantee:

  • Local strong guarantee: If an exception is thrown, program state remains unchanged with respect to the objects being manipulated. This level always implies local commit-or-rollback semantics, including that no references or iterators into a container be invalidated if an operation fails.

The "hide the details behind a pointer" technique can be done equally well by either the Widget implementer or the Widget user . If it's done by the Widget implementer, however, it's always safe, and the user won't have to do the following:

 // Example 22-3: What the user has to do if // the Widget author doesn't // 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 )     {      MyClass temp( other ); // do all the work off to the side      Swap( temp );    // then "commit" the work using      return *this;    //  nonthrowing operations only      }     // ... provide destruction, copy construction     //     and assignment that work correctly, or     //     suppress them ...    }; 

Conclusion 3: Use Pointers Judiciously

Scott Meyers writes [13] :

[13] Scott Meyers, private communication.

When I give talks on EH, I teach people two things:

1. 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:

2. 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 such as auto_ptr s, 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 ”for example by calling auto_ptr::get() ).

Often, the best way to implement the Pimpl idiom is as shown in Example 22-2 above, by using a pointer (in order to take advantage of nonthrowing operations) while still wrapping the dynamic resource safely in a manager object (in this example, an auto_ptr ). Just remember that if you do use auto_ptr , your class must provide its own destruction, copy construction, and copy assignment with the right semantics, or you can disable copy construction and assignment if those don't make sense for the class.

In the next Item, we'll apply what we've learned by using the above to analyze the best way to express a common class relationship.

I l @ ve RuBoard


More Exceptional C++
More Exceptional C++: 40 New Engineering Puzzles, Programming Problems, and Solutions
ISBN: 020170434X
EAN: 2147483647
Year: 2001
Pages: 118
Authors: Herb Sutter

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net