Return Type Covariance with Smart Pointers
Asked Answered
L

4

38

In C++ we can do this:

struct Base
{
   virtual Base* Clone() const { ... }
   virtual ~Base(){}
};

struct Derived : Base
{
   virtual Derived* Clone() const {...} //overrides Base::Clone
};

However, the following won't do the same trick:

struct Base
{
   virtual shared_ptr<Base> Clone() const { ... }
   virtual ~Base(){}
};

struct Derived : Base
{
   virtual shared_ptr<Derived> Clone() const {...} //hides Base::Clone
};

In this example Derived::Clone hides Base::Clone rather than overrides it, because the standard says that the return type of an overriding member may change only from reference(or pointer) to base to reference (or pointer) to derived. Is there any clever workaround for this? Of course one could argue that the Clone function should return a plain pointer anyway, but let's forget it for now - this is just an illustratory example. I am looking for a way to enable changing the return type of a virtual function from a smart pointer to Base to a smart pointer to Derived.

Thanks in advance!

Update: My second example indeed doesn't compile, thanks to Iammilind

Longhorn answered 3/8, 2011 at 10:4 Comment(4)
@Kerrek presumably because the Clone() might also be called in a non-polymorphic context, where it is desirable not to lose the type information.Mayworm
The proper name is covariant return types, which might improve a bit on the title.Idempotent
possible duplicate of How can I use covariant return types with smart pointers?Nephelometer
Why c++ committee solve something then introduce another trouble again and again?Shellashellac
T
53

You can't do it directly, but there are a couple of ways to simulate it, with the help of the Non-Virtual Interface idiom.

Use covariance on raw pointers, and then wrap them

struct Base
{
private:
   virtual Base* doClone() const { ... }

public:
   shared_ptr<Base> Clone() const { return shared_ptr<Base>(doClone()); }

   virtual ~Base(){}
};

struct Derived : Base
{
private:
   virtual Derived* doClone() const { ... }

public:
   shared_ptr<Derived> Clone() const { return shared_ptr<Derived>(doClone()); }
};

This only works if you actually have a raw pointer to start off with.

Simulate covariance by casting

struct Base
{
private:
   virtual shared_ptr<Base> doClone() const { ... }

public:
   shared_ptr<Base> Clone() const { return doClone(); }

   virtual ~Base(){}
};

struct Derived : Base
{
private:
   virtual shared_ptr<Base> doClone() const { ... }

public:
   shared_ptr<Derived> Clone() const
      { return static_pointer_cast<Derived>(doClone()); }
};

Here you must make sure that all overrides of Derived::doClone do actually return pointers to Derived or a class derived from it.

Tyndareus answered 3/8, 2011 at 10:42 Comment(9)
Thank you very much. This makes a lot of senseLonghorn
+1 This is the solution I would have recommended. (Well, actually, I'd have started by recommending not using a shared_ptr. It's rarely good policy for a function to return a shared_ptr. But the same solution applies to auto_ptr or other smart pointers.)Pella
I am with @James Kanze here: using a particular shared pointer in the interface forces your choice of smart pointer onto your users. Consider using a different flavor or smart pointer (unique_ptr, auto_ptr?) or a raw pointer to open up the choices to your users. (The advantage of unique_ptr or auto_ptr is that the user can acquire ownership of that pointer and pass it to a different smart pointer type, with shared_ptr the user cannot relinquish ownership)Idempotent
@James Kanze Agreed, if you are creating the object use a unique_ptr and the first solution. If you already have a shared_ptr (for example the object is being retrieved from a store, perhaps of weak_ptr's -- not likely given the function name) use the second solution.Tyndareus
A lot depends on where the pointer is coming from, and why it is being returned. If it is a newly constructed object, and the caller is expected to take over responsibility for it, unique_ptr or auto_ptr are the best solutions. If responsibility rests in the callee, or the object already exists and is being managed (possibly by itself), then a raw pointer is indicated.Pella
Can you use static_cast to convert a pointer to Base to a pointer to Derived?Taxiway
This is still one of the biggest things C++ needs to fix. It's really useful to use shared_ptr everywhere, but this is still a big hassle when doing that.Knighterrantry
@quant_dev: Sure.Lubeck
What would be the solution in case Base is a pure abstract class (just an empty interface with no implementation at all, except for virtual destructor)? I'm facing this exact situation where the production code calls the Derived's function with Base& and unit tests code calls via Derived object. (also the return types in my case are another interface type and its implementatio)Pinero
D
3

In this example Derived::Clone hides Base::Clone rather than overrides it

No, it doesn't hide it. Actually, it's a compilation error.

You cannot override nor hide a virtual function with another function that differs only on return type; so the return type should be the same or covariant, otherwise the program is illegal and hence the error.

So this implies that there is no other way by which we can convert shared_ptr<D> into shared_ptr<B>. The only way is to have B* and D* relationship (which you already ruled out in the question).

Devaluate answered 3/8, 2011 at 10:43 Comment(2)
Yes, you're right. It's an error. I'm sorry for the incorrect question :)Longhorn
The statement the return type must be same is wrong. C++ allows for covariant pointer/reference return types (covariant meaning that as you move down in one inheritance hierarchy, the return type of the override must be either the same or an object down on its own inheritance hierarchy)Idempotent
S
3

There is an improvement on a great answer by @ymett using CRTP technique. That way you needn't worry about forgetting to add a non-virtual function in the Derived.

struct Base
{
private:
   virtual Base* doClone() const { ... }

public:
   shared_ptr<Base> Clone() const { return shared_ptr<Base>(doClone()); }

   virtual ~Base(){}
};

template<class T>
struct CRTP_Base : Base
{
public:
   shared_ptr<T> Clone() const { return shared_ptr<T>(doClone()); }
};

struct Derived : public CRTP_Base<Derived>
{
private:
   virtual Derived* doClone() const { ... }
};
Salable answered 9/1, 2018 at 12:47 Comment(0)
S
0

Some ideas come to my mind. First, if you can do the first version, just leave that Clone to hide, and write another protected _clone that actually return the derived pointer. Both Clone can make use of it.

That leads to the question on why do you want to that this way. Another way could be a coerce (outside) function, in which you receive a shared_ptr<Base> and can coerce it to a shared_ptr<Derived> if possible. Maybe something along the lines of:

template <typename B, typename D>
shared_ptr<D> coerce(shared_ptr<B>& sb) throw (cannot_coerce)
{
// ...
}
Simson answered 3/8, 2011 at 10:18 Comment(2)
You mean something like, static_pointer_cast or dynamic_pointer_cast? And exception specifications blow- don't add them.Displayed
@DeadMG, yes. I didn't remember these functions right away, but something similar to them. As for the exception, OK, just was an idea on how to treat the differences in types through an intermediate step.Simson

© 2022 - 2024 — McMap. All rights reserved.