Solution

I l @ ve RuBoard

graphics/bulb.gif

A Simple ValuePtr : Strict Ownership Only

1. Write a suitable ValuePtr template that is used as shown here:

  // Example 31-1   //   class X   {   // ...   private:   ValuePtr<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 X::X() 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 X , or the X destructor must be explicitly provided, even if it's empty.

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

to suit three specific circumstances:

a) Copying and assigning ValuePtr s is not allowed.

There's really not much to it:

 // Example 31-2(a): Simple case: ValuePtr without // copying or assignment. // template<typename T> class ValuePtr { public:   explicit ValuePtr( T* p = 0 ) : p_( p ) { }   ~ValuePtr() { delete p_; } 

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? 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 ValuePtr . It may seem at first as if such facilities would be a good idea because they contribute to ValuePtr 's intended purpose and usage as a class member. Consider, for example, Examples 22-2 and 22-3 in Item 22, 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 ValuePtr s without copying the owned objects. But providing reset() - and release() -like functions isn't the right way to do it because it allows too much. It 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 ValuePtr and can cause problems if abused.

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

 void Swap( ValuePtr& other ) { swap( p_, other.p_ ); } private:   T* p_;   // no copying   ValuePtr( const ValuePtr& );   ValuePtr& operator=( const ValuePtr& ); }; 

We take ownership of the pointer and delete it afterward. 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 ValuePtr 's intended audience.

This part was pretty straightforward, but the next steps have some subtleties attached.

Copy Construction and Copy Assignment

b) Copying and assigning ValuePtr s 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 31-2(a), but with copy construction and copy assignment defined:

 // Example 31-2(b): ValuePtr with copying and // assignment, take 1. // template<typename T> class ValuePtr { public:   explicit ValuePtr( T* p = 0 ) : p_( p ) { }   ~ValuePtr() { delete p_; }   T& operator*() const { return *p_; }   T* operator->() const { return p_; }   void Swap( ValuePtr& other ) { swap( p_, other.p_ ); }  //--- new code begin ------------------------------   ValuePtr( const ValuePtr& other )   : p_( other.p_ ? new T( *other.p_ ) : 0 ) { }  

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

  ValuePtr& operator=( const ValuePtr& other )   {   ValuePtr 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 in which we will be copying or assigning from a ValuePtr that manages any type other than T . If that's all we know you'll ever need, 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: What is the impact on the Example 31-2(b) code if we want to allow for the possibility of assigning between different types of ValuePtr in the future? That is, we want to be able to copy or assign a ValuePtr<X> to a ValuePtr<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<typename U> in front and take a parameter of type ValuePtr<U>& , as follows :

 // Example 31-2(c): ValuePtr with copying and // assignment, take 2. // template<typename T> class ValuePtr { public:   explicit ValuePtr( T* p = 0 ) : p_( p ) { }   ~ValuePtr() { delete p_; }   T& operator*() const { return *p_; }   T* operator->() const { return p_; }   void Swap( ValuePtr& other ) { swap( p_, other.p_ ); }   ValuePtr( const ValuePtr& other )     : p_( other.p_ ? new T( *other.p_ ) : 0 ) { }   ValuePtr& operator=( const ValuePtr& other )   {     ValuePtr temp( other );     Swap( temp );     return *this;   }  //--- new code begin ------------------------------   template<typename U>   ValuePtr( const ValuePtr<U>& other )   : p_( other.p_ ? new T( *other.p_ ) : 0 ) { }   template<typename U>   ValuePtr& operator=( const ValuePtr<U>& other )   {   ValuePtr temp( other );   Swap( temp );   return *this;   }  private:  template<typename U> friend class ValuePtr;   //--- new code end --------------------------------  T* p_; }; 

Did you notice the trap we avoided? We still need to write the nontemplated forms of copying and assignment 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 Exceptional C++ [Sutter00] Item 5.

There is still one subtle caveat, but fortunately it's not a big deal. I'd say that it's not even our responsibility as the authors of ValuePtr . The caveat is this: With either the templated or nontemplated copy and assignment functions, the source object, other , could still 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 {}; ValuePtr<A> a1( new B ); ValuePtr<B> b1( new C ); // calls copy constructor, slices ValuePtr<A> a2( a1 ); // calls templated constructor, slices ValuePtr<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 ValuePtr documentation to warn users, preferably in a "Don't Do That" section. There's not much else we, the authors of ValuePtr , can do in code to stop this kind of abuse.

So which is the right solution to problem 1(b), Example 31-2(b) or Example 31-2(c)? 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 minimalist-design advocates would automatically use 2(b) because it's enough to satisfy the minimum requirements. I can also imagine situations in which ValuePtr is in a library written by one group and shared by several distinct teams , and in which 2(c) will end up saving overall development effort through reuse and the prevention of reinvention.

Adding Extensibility Using Traits

But what if Y has a virtual Clone() method? It may seem from Item 30 Example 30-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 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 pointed at.

What if someone wants to use a ValuePtr 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 ValuePtr s 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 ValuePtr template, we don't know what our contained T type really is; we don't know whether it has a virtual Clone() function. Therefore, 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. (For more about traits, turn to Item 4.) To implement a traits-based approach, let's first change Example 31-2(c) 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 CreateFrom() function that builds a new T object. In a moment, we'll also see another reason to do it this way.

 // Example 31-2(d): ValuePtr with copying and // assignment, Example 31-2(c) with a little // factoring. // template<typename T> class ValuePtr { public:   explicit ValuePtr( T* p = 0 ) : p_( p ) { }   ~ValuePtr() { delete p_; }   T& operator*() const { return *p_; }   T* operator->() const { return p_; }   void Swap( ValuePtr& other ) { swap( p_, other.p_ ); }   ValuePtr( const ValuePtr& other )  : p_( CreateFrom( other.p_ ) ) { } // changed  ValuePtr& operator=( const ValuePtr& other )   {     ValuePtr temp( other );     Swap( temp );     return *this;   }   template<typename U>   ValuePtr( const ValuePtr<U>& other )  : p_( CreateFrom( other.p_ ) ) { } // changed  template<typename U>   ValuePtr& operator=( const ValuePtr<U>& other )   {     ValuePtr temp( other );     Swap( temp );     return *this;   } private:  //--- new code begin ------------------------------   template<typename U>   T* CreateFrom( const U* p ) const   {   return p ? new T( *p ) : 0;   }   //--- new code end --------------------------------  template<typename U> friend class ValuePtr;   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. 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 that can be thought of as an adapter. This follows the style of char_traits , for example, where basic_string simply delegates the work of defining the character-handling policy to a traits class. (Alternatively, for example, the traits class could provide typedef s and other aids so that the ValuePtr template can figure out what to do, but still have to do it itself. I don't like that because it feels like a needless division of responsibilities. For something this simple, why have one place figure out the right thing to do, but a different place actually do it?)

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

We want VPTraits 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 ValuePtr assumes responsibility for the null check, VPTraits::Clone() doesn't have to do it; and second, if T and U are different, this function can compile only if a U* is convertible to a T* , in order to correctly handle polymorphic cases when T is Base and U is Derived .

 template<typename T> class VPTraits { public:   static T* Clone( const T* p ) { return new T( *p ); } }; 

Then VPTraits 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 VPTraits<Y> is specialized 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 VPTraits<Y> { public:   static Y* Clone( const Y* p )     { return p->CloneMe(); } }; template<> class VPTraits<Z> { public:   static Z* Clone( const Z* p )     { Z* z = new Z; p->CopyTo(*z); return z; } }; 

This is much better because the design is closed for modification but open for extension, and it will work with whatever flavor and signature of CloneMe() is ever invented in the future. Clone() only needs to create the object 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 ValuePtr with a brand new type Y that was invented centuries after ValuePtr was written and its authors turned to dust, the new fourth- millennium user (or author) of Y merely needs to specialize VPTraits once for all time, then use ValuePtr<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, but can use ValuePtr without any work at all.

A brief coda: Since VPTraits 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 ValuePtr itself is supplied in. But even then, this couples it more tightly to ValuePtr as opposed to use by other code also in that namespace. 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 typedef s or even class types? VPTraits gives us a nice place to encapsulate all those things.

ValuePtr with Traits

Here is the code for a ValuePtr that incorporates cloning traits, along with all the earlier requirements in Question 3. [3] Note that the only change from Example 31-2(d) is a one-line change to ValuePtr 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.

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

 template<typename T, typename Traits = VPTraits<T> > class ValuePtr { /*...*/ }; 

so that it's even possible for users to have different ValuePtr<X> objects in the same program copy in various ways. That additional flexibility doesn't seem to make a lot of sense in this particular case, so I'm not doing it that way ”but be aware of what's possible.

 // Example 31-2(e): ValuePtr with copying and // assignment and full traits-based // customizability. //  //--- new code begin --------------------------------   template<typename T>   class VPTraits   {   static T* Clone( const T* p ) { return new T( *p ); }   };   //--- new code end ----------------------------------  template<typename T> class ValuePtr { public:   explicit ValuePtr( T* p = 0 ) : p_( p ) { }   ~ValuePtr() { delete p_; }   T& operator*() const { return *p_; }   T* operator->() const { return p_; }   void Swap( ValuePtr& other ) { swap( p_, other.p_ ); }   ValuePtr( const ValuePtr& other )     : p_( CreateFrom( other.p_ ) ) { }   ValuePtr& operator=( const ValuePtr& other )   {     ValuePtr temp( other );     Swap( temp );     return *this;   }   template<typename U>   ValuePtr( const ValuePtr<U>& other )     : p_( CreateFrom( other.p_ ) ) { }   template<typename U>   ValuePtr& operator=( const ValuePtr<U>& other )   {     ValuePtr temp( other );     Swap( temp );     return *this;   } private:   template<typename U>   T* CreateFrom( const U* p ) const   {  //--- new code begin ----------------------------   return p ? VPTraits<U>::Clone( p ) : 0;   //--- new code end ------------------------------  }   template<typename U> friend class ValuePtr;   T* p_; }; 

A Usage Example

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

 // Example 31-3: Sample usage of ValuePtr. // 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:   ValuePtr<Y> y_; }; 

Summary

One moral I would like you to take away from this Item is to always be aware of extensibility as you're designing.

Guideline

graphics/guideline.gif

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 for years to come.

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