views:

680

answers:

6

I feel like this one has been asked before, but I'm unable to find it on SO, nor can I find anything useful on Google. Maybe "covariant" isn't the word I'm looking for, but this concept is very similar to covariant return types on functions, so I think it's probably correct. Here's what I want to do and it gives me a compiler error:

class Base;
class Derived : public Base;

SmartPtr<Derived> d = new Derived;
SmartPtr<Base> b = d; // compiler error

Assume those classes are fully fleshed out... I think you get the idea. It can't convert a SmartPtr<Derived> into a SmartPtr<Base> for some unclear reason. I recall that this is normal in C++ and many other languages, though at the moment I can't remember why.

My root question is: what is the best way to perform this assignment operation? Currently, I'm pulling the pointer out of the SmartPtr, explicitly upcasting it to the base type, then wrapping it in a new SmartPtr of the appropriate type (note that this is not leaking resources because our home-grown SmartPtr class uses intrusive reference counting). That's long and messy, especially when I then need to wrap the SmartPtr in yet another object... any shortcuts?

+3  A: 

Depends on the SmartPtr class. If it has a copy constructor (or in your case, assignment operator) that takes SmartPtr<T>, where T is the type it was constructed with, then it isn't going to work, because SmartPtr<T1> is unrelated to SmartPtr<T2> even if T1 and T2 are related by inheritance.

However, if SmartPtr has a templatized copy constructor/assignment operator, with template parameter TOther, that accepts SmartPtr<TOther>, then it should work.

Daniel Earwicker
this seems brilliant... I need to test this ASAP to see if it will work. Unfortunately, I'm using MSVC6, which has major template issues and IIRC it will barf on templatized functions inside templatized classes, even if you explicitly specify the template parameters when calling it.
rmeador
Really? - I know I have done this exact thing under MSVC6.
Eclipse
I wrote a SmartPtr class that does this right under MSVC6, so you should be okay.
Daniel Earwicker
+7  A: 

Both the copy constructor and the assignment operator should be able to take a SmartPtr of a different type and attempt to copy the pointer from one to the other. If the types aren't compatible, the compiler will complain, and if they are compatible, you've solved your problem. Something like this:

template<class Type> class SmartPtr
{
    ....
    template<class OtherType> SmartPtr(const SmartPtr<OtherType> &blah) // same logic as the SmartPtr<Type> copy constructor

    template<class OtherType> SmartPtr<Type> &operator=(const SmartPtr<OtherType> &blah) // same logic as the SmartPtr<Type> assignment operator
};
MSN
rstevens
Well yes, that's what I meant :)
MSN
+2  A: 

Assuming you have control of the SmartPtr class, the solution is to provide a templated constructor:

template <class T>
class SmartPtr
{
    T *ptr;
public:

    // Note that this IS NOT a copy constructor, just another constructor that takes 
    // a similar looking class.
    template <class O>
    SmartPtr(const SmartPtr<O> &src)
    {
        ptr = src.GetPtr();
    }
    // And likewise with assignment operator.
};

If the T and O types are compatible, it will work, if they aren't you'll get a compile error.

Eclipse
It's not always that simple; you need to ensure that the proper reference count is maintained. You can't delete the Derived object after the last Ptr<Derived> is gone when there are still Ptr<Base> to the same Derived.
MSalters
Well, yes, you'll still have to make sure that you actually implement the smarts of the smart pointer.
Eclipse
+8  A: 

SmartPtr<Base> and SmartPtr<Derived> are two distinct instantiations of a the SmartPtr template. These new classes do not share the inheritance that Base and Derived do. Hence, your problem.

what is the best way to perform this assignment operation?

 SmartPtr<Base> b = d;

Does not invoke assignment operator. This invokes the copy-ctor (the copy is elided in most cases) and is exactly as if you wrote:

 SmartPtr<Base> b(d);

Provide for a copy-ctor that takes a SmartPtr<OtherType> and implement it. Same goes for the assignment operator. You will have to write out the copy-ctor and op= keeping in mind the semantics of SmartPtr.

dirkgently
I am aware that it does not work, and I am now re-aware (thanks) that it's because of the lack of relationship between the two template classes. I'm asking how to make it behave as if they are related, perhaps by adding some clever ctor or other trickery?
rmeador
A lot depends on the exact semantics of the SmartPtr class e.g. does it have transfer of ownership, does it do reference counting etc. You will have to write out the copy-ctor and op= keeping in mind the semantics of SmartPtr.
dirkgently
+2  A: 

Templates are not covariant, and that's good; imagine what would happen in the following case:

vector<Apple*> va;
va.push_back(new Apple);

// Now, if templates were covariants, a vector<Apple*> could be
// cast to a vector<Fruit*>
vector<Fruit*> & vf = va;
vf.push_back(new Orange); // Bam, we just added an Orange among the Apples!

To achieve what you are trying to do, the SmartPointer class must have a templatized constructor, that takes either another SmartPointer or a pointer of another type. You could have a look at boost::shared_ptr, which does exactly that.

template <typename T>
class SmartPointer {

    T * ptr;

  public:
    SmartPointer(T * p) : ptr(p) {}
    SmartPointer(const SmartPointer & sp) : ptr(sp.ptr) {}

    template <typename U>
    SmartPointer(U * p) : ptr(p) {}

    template <typename U>
    SmartPointer(const SmartPointer<U> & sp) : ptr(sp.ptr) {}

    // Do the same for operator= (even though it's not used in your example)
};
Luc Touraille
You make an excellent point with your example... I hadn't considered that. In the case of a smart pointer, however, I don't think you run into that problem because the class interface is essentially the same as the pointed-to type's interface (via overloaded -> and *).
rmeador
A: 

I think the easiest thing is to provide automatic conversion to another SmartPtr according to the following:

template <class T>
class SmartPtr
{
public:
    SmartPtr(T *ptr) { t = ptr; }
    operator T * () const { return t; }
    template <class Q> operator SmartPtr<Q> () const
    { return SmartPtr<Q>(static_cast<Q *>(static_cast<T *>(* this))); }
private:
    T *t;
};

Note that this implementation is robust in the sense that the conversion operator template does not need to know about the semantics of the smart pointer, so reference counting does not need to replicated etc.

antti.huima
Try { return SmartPtr<Q>(t); } The compiler will tell you if a T* can be assigned to Q* without all the casts. Be sure your reference count logic can share the reference count between template types. An int* reference count should be able to.
jmucchiello