21

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?

rmeador
  • 25,504
  • 18
  • 62
  • 103

6 Answers6

15

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
  • 108,024
  • 16
  • 131
  • 187
  • 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 Mar 12 '09 at 16:00
  • 2
    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 Mar 12 '09 at 16:03
13

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
  • 53,214
  • 7
  • 75
  • 105
  • hide comments @MSN: I learned by trial-and-error that this is not enough to fulfill the "normal" copy constructor and assignment operator. So you have to implement both: SmartPtr(const SmartPtr &) AND template SmartPtr(const SmartPtr &) (same for op=) – mmmmmmmm Mar 12 '09 at 17:20
  • Well yes, that's what I meant :) – MSN Mar 12 '09 at 17:36
  • From C++11 onwards you also add move-constructor and move-assignment (those with `&&`). – Pablo H Oct 11 '17 at 20:28
4

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
  • 79,925
  • 15
  • 92
  • 137
  • 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 Mar 12 '09 at 17:51
  • 5
    I think your example is a mis-example in that it is not actually covariant exactly _because_ you can shove something like an Orange in the list. It will only be covariant if the public interface only ever _returns_ Fruit*, but not _accepts_ Fruit*. C# provides the "in" and "out" keywords to make generic types covariant or contravariant, respectively. It statically enforces how the types are used to avoid the situation you described. – LostSalad Jun 06 '14 at 11:56
3

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
  • 114,894
  • 38
  • 205
  • 284
  • 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 Mar 12 '09 at 16:01
  • I wrote a SmartPtr class that does this right under MSVC6, so you should be okay. – Daniel Earwicker Mar 12 '09 at 16:33
2

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
  • 44,851
  • 20
  • 112
  • 171
  • 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 is gone when there are still Ptr to the same Derived. – MSalters Mar 12 '09 at 15:59
  • Well, yes, you'll still have to make sure that you actually implement the smarts of the smart pointer. – Eclipse Mar 12 '09 at 16:07
0

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
  • 25,136
  • 3
  • 52
  • 71
  • Try { return SmartPtr(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 Sep 30 '09 at 21:30