• No results found

ValuePtr temp( other ); Swap( temp );

In document More Exceptional C++ pdf (Page 182-186)

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;

}

In document More Exceptional C++ pdf (Page 182-186)