Solution

I l @ ve RuBoard

graphics/bulb.gif

Recap: Problems of Pointer Members

1. Consider the following class:

  // Example 30-1   //   class X1   {   // ...   private:   Y* y_;   };  

If an X1 object owns its pointed-at Y object, why can't the author of X1 use the compiler-generated destructor, copy constructor, and copy assignment?

If X1 owns its pointed-at Y , the compiler-generated versions will do the wrong thing. First, note that some function (probably an X1 constructor) has to create the owned Y object, and there has to be another function (likely the destructor X1::~X1() ) that deletes it:

 // Example 30-1(a): Ownership semantics. // {   X1 a; // allocates a new Y object and points at it   // ... } // as a goes out of scope and is destroyed, it   // deletes the pointed-at Y 

Then use of the default memberwise copy construction will cause multiple X1 objects to point at the same Y object, which will cause strange results as modifying one X1 object changes the state of another, and which will also cause a double delete to take place:

 // Example 30-1(b): Sharing, and double delete. // {   X1 a;   // allocates a new Y object and points at it   X1 b( a ); // b now points at the same Y object as a   // ... manipulating a and b modifies   // the same Y object ... } // as b goes out of scope and is destroyed, it   // deletes the pointed-at Y... and so does a, oops 

Any use of the default memberwise copy assignment will also cause multiple X1 objects to point at the same Y object, which will cause the same state sharing, the same double delete , and as an added bonus will also cause leaks when some objects are never deleted at all:

 // Example 30-1(c): Sharing, double delete, plus leak. // {   X1 a;  // allocates a new Y object and points at it   X1 b;  // allocates a new Y object and points at it   b = a; // b now points at the same Y object as a,          // and no one points at the Y object that          // was created by b   // ... manipulating a and b modifies   // the same Y object ... } // as b goes out of scope and is destroyed, it   // deletes the pointed-at Y... and so does a, oops   // the Y object allocated by b is never deleted 

In other code, we normally apply the good practice of wrapping bald pointers in manager objects that own them and simplify cleanup. If the Y member was held by such a manager object instead of by a bald pointer, wouldn't that ameliorate the situation? This brings us to the following key questions.

What About auto_ptr Members?

2. What are the advantages and drawbacks of the following approach?

  // Example 30-2   //   class X2   {   // ...   private:   auto_ptr<Y> y_;   };  

This has some benefits, but it doesn't do a whole lot to solve the problem that the automatically generated copy construction and copy assignment functions will do the wrong thing. It just makes them do different wrong things.

First, if X2 has any user -written constructors, making them exception-safe is easier because if an exception is thrown in the constructor, the auto_ptr will perform its cleanup automatically. The writer of X2 is still forced, however, to allocate his own Y object and hold it, however briefly , by a bald pointer before the auto_ptr object assumes ownership.

Next, the automatically generated destructor now does, in fact, do the right thing. As an X2 object goes out of scope and is destroyed, the auto_ptr<Y> destructor automatically performs cleanup by deleting the owned Y object. Even so, there is a subtle caveat here that has already caught me once: If you rely on the automatically generated destructor, then that destructor will be defined in each translation unit that uses X2 . That means that the definition of Y must be visible to pretty much anyone who uses an X2 . This is not so good if Y is a Pimpl, for example, and the whole point is to hide Y 's definition from clients of X2 . So you can rely on the automatically generated destructor, but only if the full definition of Y is supplied along with X2 (for example, if x2 . h includes y . h ):

 // Example 30-2(a): Y must be defined. // {   X2 a; // allocates a new Y object and points at it   // ... } // as a goes out of scope and is destroyed, it   // deletes the pointed-at Y, and this can only   // happen if the full definition of Y is available 

If you don't want to provide the definition of Y , you must write the X2 destructor explicitly, even if it's empty.

Next, the automatically generated copy constructor will no longer have the double- delete problem described in Example 30-1(b). That's the good news. The not-so-good news is that the automatically generated version introduces another problem ” grand theft. The X2 object being constructed rips away the Y belonging to the copied -from X2 object, including all knowledge of the Y object.

 // Example 30-2(b): Grand theft pointer. // {   X2 a; // allocates a new Y object and points at it   X2 b( a ); // b rips away a's Y object, leaving a's              // y_ member with a null auto_ptr   // if a attempts to use its y_ member, it won't   // work; if you're lucky, the problem will manifest   // as an immediate crash, otherwise it will likely   // manifest as a difficult-to-diagnose intermittent   // failure } 

The only redeeming point about the grand theft, and it isn't much, is that at least the automatically generated X2 copy constructor gives some fair warning that theft-like behavior may be impending . [1] Why? Because its signature will be X2::X2( X2& ) . Note that it takes its parameter by reference to non- const . That's what auto_ptr 's copy constructor does, after all, and so X2 's automatically generated one has to follow suit. This is pretty subtle, though, but at least it prevents copying from a const X2 .

[1] Alas, the non- const ness of the parameter is rather invisible. The function is silently generated by the compiler, and you never actually get to see the signature appear as human-readable code anywhere .

Finally, the automatically generated copy assignment operator will no longer have either the double- delete problem or the leak problem, both of which were described in Example 30-1(c). That's the good news. Alas, again, there's some not-so-good news. The same grand theft occurs: The assigned-to X2 object rips away the Y belonging to assigned-from X2 object, including all knowledge of the Y object, and in addition it (possibly prematurely) deletes the Y object it originally owned.

 // Example 30-2(c): More grand theft pointer. // {   X2 a;  // allocates a new Y object and points at it   X2 b;  // allocates a new Y object and points at it   b = a; // b deletes its pointed-at Y, rips away          // a's Y, and leaves a with a null auto_ptr          // again   // as in Example 30-2(b), any attempts by a to use its   // y_ member will be disastrous } 

Similarly above, at least the thieving behavior is hinted at, because the automatically generated function will be declared as X2& X2::operator=( X2& ) , thus advertising (albeit in the fine print, not in a front-page banner headline) that the operand can be modified.

In summary, then, auto_ptr does give some benefits, particularly by automating cleanup for constructors and the destructor. It does not, however, of itself answer the main original problems in this case ”that we have to write our own copy construction and copy assignment for X2 or else disable them if copying doesn't make sense for the class. For that, we can do better with something a little more special-purpose.

Variations on a Theme by ValuePtr

The meat of this miniseries of Items involves developing successively refined versions of a ValuePtr template that is more suitable than auto_ptr for the kinds of uses outlined above.

A note on exception specifications: For reasons I won't go into here, exception specifications are not as useful as you might think. On the other hand, it is important to know which exceptions a function might throw, especially if it is known to throw none at all, giving the nothrow guarantee. You don't need exception specifications to document behavior, so I am going to assert the following:

For every version of ValuePtr<T> presented in these two Items, all member functions provide the nothrow guarantee except that construction or assignment from a ValuePtr<U> (where U could be T ) might emit an exception thrown from a T constructor .

Now let's get into the meat of it in Item 31.

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