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 thanT
. 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 aValuePtr<X>
to aValuePtr<Y>
ifX
is convertible toY
.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 typeValuePtr<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 ofValuePtr
, 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 virtualClone()
method? It may seem from Item 30 Example 30-1 thatX
always creates its own ownedY
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 ownedY
object might not really be aY
object at all, but of some type derived fromY
, and copying it as aY
would slice it at best and render it unusable at worst. The usual technique in this kind of situation is forY
to provide a special virtualClone()
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 ownedY
object, which is performed using a virtualY::Clone()
method if present and theY
copy constructor otherwise.In the
ValuePtr
template, we don't know what our containedT
type really is; we don't know whether it has a virtualClone()
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