GotW #62

Home Blog Talks Books & Articles Training & Consulting

On the
blog
RSS feed November 4: Other Concurrency Sessions at PDC
November 3
: PDC'09: Tutorial & Panel
October 26: Hoare on Testing
October 23
: Deprecating export Considered for ISO C++0x

This is the original GotW problem and solution substantially as posted to Usenet. See the book More Exceptional C++ (Addison-Wesley, 2002) for the most current solution to this GotW issue. The solutions in the book have been revised and expanded since their initial appearance in GotW. The book versions also incorporate corrections, new material, and conformance to the final ANSI/ISO C++ standard.

Smart Pointer Members 
Difficulty: 6 / 10

Most C++ programmers know they have to take special care for classes with pointer members. But what about classes with auto_ptr members? And can we make life safer for ourselves and our users by devising a smart pointer class designed specifically for class membership?

Problem

JG Question

1. Consider the following class:

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

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

Guru Questions

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

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

3. Write a suitable HolderPtr template that is used as shown here:

// Example 3
//
class X3
{
  // ...
private:
  HolderPtr<Y> y_;
};

to suit three specific circumstances:

a) Copying and assigning HolderPtrs is not allowed.

b) Copying and assigning HolderPtrs is allowed, and has the semantics of creating a copy of the owned Y object using the Y copy constructor.

c) Copying and assigning HolderPtrs is allowed, and has the semantics of creating a copy of the owned Y object, which is performed using a virtual Y::Clone() method if present and the Y copy constructor otherwise.

Solution

Recap: Problems of Pointer Members

1. Consider the following class:

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

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

// Example 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 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

And 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 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 our Guru Questions:

What About auto_ptr Members?

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

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

In short, 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, which 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 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, then you must write the X2 destructor explicitly, even if it is just empty.

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

// Example 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 above grand theft, and it isn't much, is that at least the automatically generated X2 copy constructor gives some fair warning that theftlike behavior may be impending. Why? Because its signature will be X2::X2( X2& ) -- note that it takes it 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.

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 1(c). That's the good news. Alas, again, there's some not-so-good news, because 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 that it originally owned:

// Example 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 2(b), any attempts by a to use its
  // y_ member will be disastrous
}

Similarly above, at least the theftish 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) 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 HolderPtr

The meat of this article involves development successively refined versions of a HolderPtr 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 (see a coming issue of GotW), exception specifications are not as useful as you might think. On the other hand, it is important to know what exceptions a function might throw, especially if it is AC-safe (always succeeds and leaves the system in a consistent state) which means no exception can occur; this is also known as a nothrow guarantee. Well, you don't need exception specifications to document behavior, and so I am going to assert the following:

For every version of HolderPtr<T> presented in this article, all member functions are AC-safe (nothrow) except that construction or assignment from a HolderPtr<U> (where U could be T) might cause an exception to be thrown from a T constructor.

Now let's get into the meat of it:

A Simple HolderPtr: Strict Ownership Only

3. Write a suitable HolderPtr template that is used as shown here:

// Example 3
//
class X3
{
  // ...
private:
  HolderPtr<Y> y_;
};

We are going to consider three cases. In all three, the constructor benefit still applies: Cleanup is automated and there's less work for the writer of X3::X3() to be exception-safe and avoid leaks from failed constructors.[2] Also, in all three, the destructor restriction still applies: Either the full definition of Y must accompany X3, or the X3 destructor must be explicitly provided, even if it's empty.

to suit three specific circumstances:

a) Copying and assigning HolderPtrs is not allowed.

There's really not much to it:

// Example 3(a): Simple case: HolderPtr without
// copying or assignment.
//
template<class T>
class HolderPtr
{
public:
  explicit HolderPtr( T* p = 0 ) : p_( p ) { }

  ~HolderPtr() { delete p_; p_ = 0; }

Of course, there has to be some way to access the pointer, so add something like the following, which parallels std::auto_ptr:

  T& operator*() const { return *p_; }

  T* operator->() const { return p_; }

What else might we need? Well, for many smart pointer types it can make sense to provide additional facilities that parallel auto_ptr's reset() and release() functions to let users arbitrarily change which object is owned by a HolderPtr. It may seem at first like that such facilities would be a good idea because they contribute to HolderPtr's intended purpose and usage as a class member; consider, for example, Example 4 in the solution to GotW #59, where the class member holds a Pimpl pointer and it's desirable to write an exception-safe assignment operator... then you need a way to swap HolderPtrs without copying the owned objects. But providing reset()- and release()-like functions isn't the right way to do it... that would let users do what they need for swapping and exception-safety, but it would also open the door for many other (unneeded) options that don't contribute to the purpose of HolderPtr and can cause problems if abused.

So what to do? Insteading of providing overly general facilities, understand your requirements well enough to provide just the facility you really need:

  void Swap( HolderPtr& other ) { swap( p_, other.p_ ); }

private:
  T* p_;

  // no copying
  HolderPtr( const HolderPtr& );
  HolderPtr& operator=( const HolderPtr& );
};

We take ownership of the pointer and delete it afterwards, we handle the null pointer case, and copying and assignment are specifically disabled in the usual way by declaring them private and not defining them. Construction is explicit as good practice to avoid implicit conversions, which are never needed by HolderPtr's intended audience.

There, that was easy. I hope it didn't lull you into a false sense of security, because the next steps have some subtleties attached.

Copy Construction and Copy Assignment

b) Copying and assigning HolderPtrs is allowed, and has the semantics of creating a copy of the owned Y object using the Y copy constructor.

Here's one way to write it that satisfies the requirements but isn't as general-purpose as it could be. It's the same as Example 3(a), but with copy construction and copy assignment defined:

// Example 3(b)(i): HolderPtr with copying and
// assignment, take 1.
//
template<class T>
class HolderPtr
{
public:
  explicit HolderPtr( T* p = 0 ) : p_( p ) { }

  ~HolderPtr() { delete p_; p_ = 0; }

  T& operator*() const { return *p_; }

  T* operator->() const { return p_; }

  void Swap( HolderPtr& other ) { swap( p_, other.p_ ); }

  //--- new code begin ------------------------------
  HolderPtr( const HolderPtr& other )
    : p_( other.p_ ? new T( *other.p_ ) : 0 ) { }

Note that it's important to check whether other's pointer is null or not. Since, however, operator=() is implemented in terms of copy construction, we only have to put the check in one place.

  HolderPtr& operator=( const HolderPtr& other )
  {
    HolderPtr<T> temp( other );
    Swap( temp );
    return *this;
  }
  //--- new code end --------------------------------

private:
  T* p_;
};

This satisfies the stated requirements, because in the intended usage there's no case where we will be copying or assigning from a HolderPtr that manages any type other than T. If that's all we know you'll ever need, then that's fine. But whenever we design a class, we should at least consider designing for extensibility if it doesn't cost us much extra work and could make the new facility more useful to users in the future. At the same time, we need to balance such "design for reusability" with the danger of overengineering, that is, of providing an overly complex solution to a simple problem. This brings us to the next point:

Templated Construction and Templated Assignment

One question to consider is this: What is the impact on the Example 3(b)(i) code if we want to allow for the possibility of assigning between different types of HolderPtr in the future? That is, we want to be able to copy or assign a HolderPtr<X> to a HolderPtr<Y> if X is convertible to Y. It turns out that the impact is minimal: Duplicate the copy constructor and the copy assignment operators with templated versions that just add "template<class U>" in front and take a parameter of type "HolderPtr<U>&", as follows:

// Example 3(b)(ii): HolderPtr with copying and
// assignment, take 2.
//
template<class T>
class HolderPtr
{
public:
  explicit HolderPtr( T* p = 0 ) : p_( p ) { }

  ~HolderPtr() { delete p_; p_ = 0; }

  T& operator*() const { return *p_; }

  T* operator->() const { return p_; }

  void Swap( HolderPtr& other ) { swap( p_, other.p_ ); }

  HolderPtr( const HolderPtr& other )
    : p_( other.p_ ? new T( *other.p_ ) : 0 ) { }

  HolderPtr& operator=( const HolderPtr& other )
  {
    HolderPtr<T> temp( other );
    Swap( temp );
    return *this;
  }

  //--- new code begin ------------------------------
  template<class U>
  HolderPtr( const HolderPtr<U>& other )
    : p_( other.p_ ? new T( *other.p_ ) : 0 ) { }

  template<class U>
  HolderPtr& operator=( const HolderPtr<U>& other )
  {
    HolderPtr<T> temp( other );
    Swap( temp );
    return *this;
  }

private:
  template<class U> friend class HolderPtr;
  //--- new code end --------------------------------

T* p_;
};

Did you notice the trap we avoided? We still need to write the nontemplated forms of copying and assignment too in order to suppress the automatically generated versions, because a templated constructor is never a copy constructor and a templated assignment operator is never a copy assignment operator. For more information about this, see Item 5 in Exceptional C++.[1]

Balancing reusability against overengineering

From what I understand of XP, it seems to me that migration from 3(b)(i) to 3(b)(ii) may happen in an XP environment only if HolderPtr is used in a single project, or is owned by the same team that comes across the new requirement to copy and assign between different kinds of HolderPtr. Part of the core XP philosophy is to only refactor or extend when necessary to implement a particular new feature, and that assumes that the person implementing the feature also has rights to extend the code being reused, here HolderPtr; if not, you'll end up having several project teams each writing their own extensions as needed when a reusable common component is insufficient, instead of upgrading the common component once and saving overall effort across projects. (And that's just the "upgrade/extend" case; it's even harder to refactor common components already in use in multiple projects even if you do have the source rights, because the need to maintain interface stability greatly limits the scope of acceptable changes.)

This kind of situation seems that it might build into XP a systemic impediment to "design for extensibility" at least for library creation and maintenance and other cross-project reuse efforts, and is part of the cost for the short-term rather than long-term thinking encouraged by XP. That's not to say that XP's advantages may not outweigh the disadvantages in a given situation -- I'm sure there are situations where they do --, but it is important to understand both sides of a particular methodology before jumping into it with both feet for a particular project. For example, XP has a lot to recommend it for a single, short-term, focused project that does not overlap much or at all with other projects and can be thrown away if needed, but XP might not be as appropriate for writing things like space shuttle onboard systems, or even shared libraries unless the library team was treated as a distinct project.

This is my feeling only, based on what little I know of XP. Corrections and flames are welcome.

There is still one subtle caveat, though, but fortunately it's not really a big deal (or even, I would say, our responsibility as the authors of HolderPtr): With either the templated or nontemplated copy and assignment functions, the source ("other") object could still actually be holding a pointer to a derived type in which case we're slicing. For example:

class A {};
class B : public A {};
class C : public B {};

HolderPtr<A> a1( new B );
HolderPtr<B> b1( new C );

// calls copy ctor,
// slices
HolderPtr<A> a2( a1 );

// calls templated ctor,
// slices
HolderPtr<A> a3( b1 );

// calls copy assignment,
// slices
a2 = a1;

// calls templated
// assignment, slices
a3 = b1;

I point this out because this is the sort of thing one shouldn't forget to write up in the HolderPtr documentation to warn users, preferably in a "Don't Do That" section. There's not much else we the authors of HolderPtr can do in code to stop this kind of abuse.

So which is the right solution to problem 3(b) -- Example 3(b)(i), or Example 3(b)(ii)? Both are good solutions, and it's really a judgment call based on your own experience at balancing design-for-reuse and overengineering-avoidance. I imagine that XP advocates in particular would automatically use 3(b)(i) because it satisfies the minimum requirements. I can also imagine situations where HolderPtr is in a library written by one group and shared by several distinct teams and where 3(b)(ii) will end up saving overall development effort through reuse and the prevention of reinvention.

Adding Extensibility Using Traits

But, now, what if Y has a virtual Clone() method? It may seem from Example 1 that X always creates its own owned Y object, but it might get it from a factory or from a new-expression of some derived type. As we've already seen, in such a case the owned Y object might not really be a Y object at all, but be of some type derived from Y, and copying it as a Y would slice it at best and render it unusable at worst. The usual technique in this kind of situation is for Y to provide a special virtual Clone() member function that allows complete copies to be made even without knowing the complete type of the object being pointed at.

So, what if someone wants to use a HolderPtr to hold such an object, that can only be copied using a function other than the copy constructor? This is the point of our final question:

c) Copying and assigning HolderPtrs is allowed, and has the semantics of creating a copy of the owned Y object, which is performed using a virtual Y::Clone() method if present and the Y copy constructor otherwise.

In the HolderPtr template, we don't know what our contained T type really is, we don't know whether it has a virtual Clone() function, and so we don't know the right way to copy it. Or do we?

One solution is to apply a technique widely used in the C++ standard library itself, namely traits. Briefly, a traits class is defined as follows, quoting clause 17.1.18:

a class that encapsulates a set of types and functions necessary for template classes and template functions to manipulate objects of types for which they are instantiated

First, let's change Example 3(b)(ii) slightly to remove some redundancy. You'll notice that both the templated constructor and the copy constructor have to check the source for nullness. Let's put all that work in a single place and have a single Create() function that builds a new T object (we'll see another reason to do this in a minute):

// Example 3(c)(i): HolderPtr with copying and
// assignment, Example 3(b)(ii)
// with a little factoring.
//
template<class T>
class HolderPtr
{
public:
  explicit HolderPtr( T* p = 0 ) : p_( p ) { }

  ~HolderPtr() { delete p_; p_ = 0; }

  T& operator*() const { return *p_; }

  T* operator->() const { return p_; }

  void Swap( HolderPtr& other ) { swap( p_, other.p_ ); }

  HolderPtr( const HolderPtr& other )
    : p_( CreateFrom( other.p_ ) ) { } // changed

  HolderPtr& operator=( const HolderPtr& other )
  {
    HolderPtr<T> temp( other );
    Swap( temp );
    return *this;
  }

  template<class U>
  HolderPtr( const HolderPtr<U>& other )
    : p_( CreateFrom( other.p_ ) ) { } // changed

  template<class U>
  HolderPtr& operator=( const HolderPtr<U>& other )
  {
    HolderPtr<T> temp( other );
    Swap( temp );
    return *this;
  }

private:
  //--- new code begin ------------------------------
  template<class U>
  T* CreateFrom( const U* p ) const
  {
    return p ? new T( *p ) : 0;
  }
  //--- new code end --------------------------------

  template<class U> friend class HolderPtr;

  T* p_;
};

Now, CreateFrom() gives us a nice hook to encapsulate all knowledge about different ways of copying a T.

Applying Traits

Now we can apply the traits technique using something like the following approach. Note that this is not the only way to apply traits, and there are other ways besides traits to deal with different ways of copying a T. I chose to use a single traits class template with a single static Clone() member that calls whatever is needed to do the actual cloning, and which can be thought of as an adapter. This follows the style of char_traits, for example, which simply delegates the work to the traits class. (Alternatively, for example, the traits class could provide typedefs and other aids so that the HolderPtr template to figure out what to do, but still have to do it itself, but it's a needless division of responsibilities to have one place find out what the right thing to do is, and a different place to actually do it.)

template<class T>
class HolderPtr
{
  // ...

  template<class U>
  T* CreateFrom( const U* p ) const
  {
    // the "Do the Right Thing" fix... but how?
    return p ? HPTraits<U>::Clone( p ) : 0;
  }
};

We want HPTraits to be a template that does the actual cloning work, where the main template's implementation of Clone() uses U's copy constructor. Two notes: First, since HolderPtr assumes responsibility for the null check, HPTraits::Clone() doesn't have to do it; and second, if T and U are different this function can only compile if a U* is convertible to a T*, in order to correctly handle polymorphic cases like T==Base U==Derived.

template<class T>
class HPTraits
{
  static T* Clone( const T* p ) { return new T( *p ); }
};

Then HPTraits is specialized as follows for any given Y that does not want to use copy construction. For example, say that some Y has a virtual Y* CloneMe() function, and some Z has a virtual void CopyTo( Z& ) function; then HPTraits<Y> is specialized so as to let that function do the cloning:

// The most work any user has to do, and it only
// needs to be done once, in one place:
//
template<>
class HPTraits<Y>
{
  static Y* Clone( const Y* p )
    { return p->CloneMe(); }
};

template<>
class HPTraits<Z>
{
  static Z* Clone( const Z* p )
    { Z* z = new Z; p->CopyTo(*z); return z; }
};

This is much better, and it will work with whatever flavor and signature of CloneMe() is ever invented in the future; Clone() only needs to create it under the covers in whatever way it deems desirable, and the only visible result is a pointer to the new object... another good argument for strong encapsulation.

To use HolderPtr with a brand new type Y that was invented centuries after HolderPtr was written and its authors turned to dust, the new fourth-millennium user (or author) of Y merely needs to specialize HPTraits once for all time, then use HolderPtr<Y>s all over the place in her code wherever desired. That's pretty easy. And, if Y doesn't have a virtual Clone() function, the user (or author) of Y doesn't even need to do that and can just use HolderPtr without any work at all.

A brief coda: Since HPTraits has only a single static function template, why make it a class template instead of just a function template? The main motive is for encapsulation (in particular, better name management) and extensibility. We want to avoid cluttering the global namespace with free functions; the function template could be put at namespace scope in whatever namespace HolderPtr itself is supplied in, but even then this couples it more tightly to HolderPtr as opposed to use by perhaps other code also in that namespace. Now, the Clone() function template may be the only kind of trait we need today, but what if we need new ones tomorrow? If the additional traits are functions, we'd otherwise have to continue cluttering things up with extra free functions. But what if the additional traits are typedefs or even class types? HPTraits gives us a nice place to encapsulate all of those things.

HolderPtr With Traits

Here is the code for a HolderPtr that incorporates cloning traits along with all of the earlier requirements in Question 3.[3] Note that the only change from Example 3(c)(i) is a one-line change to HolderPtr and one new template with a single simple function... now that's what I call minimum-impact given all of the flexibility we've just bought:

// Example 3(c)(ii): HolderPtr with copying and
// assignment and full traits-based
// customizability.
//
//--- new code begin --------------------------------
template<class T>
class HPTraits
{
static T* Clone( const T* p ) { return new T( *p ); }
};
//--- new code end ----------------------------------

template<class T>
class HolderPtr
{
public:
  explicit HolderPtr( T* p = 0 ) : p_( p ) { }

  ~HolderPtr() { delete p_; p_ = 0; }

  T& operator*() const { return *p_; }

  T* operator->() const { return p_; }

  void Swap( HolderPtr& other ) { swap( p_, other.p_ ); }

  HolderPtr( const HolderPtr& other )
    : p_( CreateFrom( other.p_ ) ) { }

  HolderPtr& operator=( const HolderPtr& other )
  {
    HolderPtr<T> temp( other );
    Swap( temp );
    return *this;
  }

  template<class U>
  HolderPtr( const HolderPtr<U>& other )
    : p_( CreateFrom( other.p_ ) ) { }

  template<class U>
  HolderPtr& operator=( const HolderPtr<U>& other )
  {
    HolderPtr<T> temp( other );
    Swap( temp );
    return *this;
  }

private:
  template<class U>
  T* CreateFrom( const U* p ) const
  {
  //--- new code begin ----------------------------
    return p ? HPTraits<U>::Clone( p ) : 0;
  //--- new code end ------------------------------
  }

  template<class U> friend class HolderPtr;

  T* p_;
};

A Usage Example

Here is an example that shows the typical implementation of the major functions (construction, destruction, copying, and assignment) for class that uses our final version of HolderPtr, ignoring more detailed issues like whether the destructor ought or ought not to be present (or inline) because Y is or is not defined:

// Example 4: Sample usage of HolderPtr.
//
class X
{
public:
  X() : y_( new Y(/*...*/) ) { }

  ~X() { }

  X( const X& other ) : y_( new Y(*(other.y_) ) ) { }

  void Swap( X& other ) { y_.Swap( other.y_ ); }

  X& operator=( const X& other )
  {
    X temp( other );
    Swap( temp );
    return *this;
  }

private:
  HolderPtr<Y> y_;
};

Summary

One moral I would like you to take away from this issue of GotW is to always be aware of extensibility as you're designing. By default, prefer to design for reuse. While avoiding the trap of overengineering, always be aware of a longer-term view -- you can always decide to reject it, but always be aware of it -- so that you save time and effort in the long run both for yourself and for all the grateful users who will be happily reusing your code with their own new classes well into the new millennium.

 

Notes

1. H. Sutter. Exceptional C++ (Addison-Wesley, 2000).

2. It is also possible to have HolderPtr itself perform the construction of the owned Y object, but I will omit that for clarity and because it pretty much just gives HolderPtr<Y> the same Y value semantics, begging the question "then why not just use a plain old Y member?"

3. Alternatively, one might also choose to provide a traits object as an additional HolderPtr template parameter Traits that just defaults to HPTraits<T>, the same way std::basic_string does:

template<class T, class Traits = HPTraits<T> >
class HolderPtr { /*...*/ };

so that it's even possible for users to have different HolderPtr<X> objects in the same program copy in different ways. That didn't seem to make a lot of sense in this particular case, so I didn't do it, but it shows what's possible.

Copyright © 2009 Herb Sutter