How can I use covariant return types with smart pointers?
Firstly, this is indeed how it works in C++: the return type of a virtual function in a derived class must be the same as in the base class. There is the special exception that a function that returns a reference/pointer to some class X can be overridden by a function that returns a reference/pointer to a class that derives from X, but as you note this doesn't allow for smart pointers (such as shared_ptr
), just for plain pointers.
If your interface RetInterface
is sufficiently comprehensive, then you won't need to know the actual returned type in the calling code. In general it doesn't make sense anyway: the reason get_r
is a virtual
function in the first place is because you will be calling it through a pointer or reference to the base class AInterface
, in which case you can't know what type the derived class would return. If you are calling this with an actual A1
reference, you can just create a separate get_r1
function in A1
that does what you need.
class A1: public AInterface
{
public:
boost::shared_ptr<RetInterface> get_r() const
{
return get_r1();
}
boost::shared_ptr<Ret1> get_r1() const {...}
...
};
Alternatively, you can use the visitor pattern or something like my Dynamic Double Dispatch technique to pass a callback in to the returned object which can then invoke the callback with the correct type.
Return Type Covariance with Smart Pointers
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.
How to accomplish covariant return types when returning a shared_ptr?
I think that a solution is fundamentally impossible because covariance depends on pointer arithmetic which is incompatible with smart pointers.
When Y::foo
returns shared_ptr<B>
to a dynamic caller, it must be cast to shared_ptr<A>
before use. In your case, a B*
can (probably) simply be reinterpreted as an A*
, but for multiple inheritance, you would need some magic to tell C++ about static_cast<A*>(shared_ptr<B>::get())
.
What are the drawbacks of C++ covariance return types?
The chief limitation of covariant return types as implemented in C++ is that they only work with raw pointers and references. There are no real reasons not to use them when possible, but the limitation means we cannot always use them when we need them.
It is easy to overcome this limitation while providing identical user experience, without ever relying to the language feature. Here's how.
Let's rewrite our classes using the common and popular non-virtual interface idiom.
struct AbstractFactory
{
Base *create() {
return create_impl();
}
private:
virtual Base* create_impl() = 0;
};
struct ConcreteFactory : public AbstractFactory
{
Derived *create() {
return create_impl();
}
private:
Derived *create_impl() override {
return new Derived;
}
};
Now here something interesting happens. create
is no longer virtual, and therefore can have any return type. It is not constrained by the covariant return types rule. create_impl
is still constrained, but it's private, no one is calling it but the class itself, so we can easily manipulate it and remove covariance altogether.
struct ConcreteFactory : public AbstractFactory
{
Derived *create() {
return create_impl();
}
private:
Base *create_impl() override {
return create_impl_derived();
}
virtual Derived *create_impl_derived() {
return new Derived;
}
};
Now both AbstractFactory
and ConcreteFactory
has exactly the same interface as before, without a covariant return type in sight. What does it mean for us? It means we can use smart pointers freely.
// replace `sptr` with your favourite kind of smart pointer
struct AbstractFactory
{
sptr<Base> create() {
return create_impl();
}
private:
virtual sptr<Base> create_impl() = 0;
};
struct ConcreteFactory : public AbstractFactory
{
sptr<Derived> create() {
return create_impl();
}
private:
sptr<Base> create_impl() override {
return create_impl_derived();
}
virtual sptr<Derived> create_impl_derived() {
return make_smart<Derived>();
}
};
Here we overcame a language limitation and provided an equivalent of covariant return types for our classes without relying on a limited language feature.
Note for the technically inclined.
sptr<Base> create_impl() override {
return create_impl_derived();
}
This here function implicitly converts ("upcasts") a Derived
pointer to a Base
pointer. If we use covariant return types as provided by the language, such upcast is inserted by the compiler automatically when needed. The language is unfortunately only smart enough to do it for raw pointers. For everything else we have to do it ourselves. Luckily, it's relatively easy, if a bit verbose.
(In this particular case it could be acceptable to just return a Base
pointer throughout. I'm not discussing this. I'm assuming we absolutely need something like covariant return types.)
How do `shared_ptr`s achieve covariance?
Yes, specializations of the same class template by default have almost no relationship and are essentially treated like unrelated types. But you can always define implicit conversions between class types by defining converting constructors (To::To(const From&)
) and/or conversion functions (From::operator To() const
).
So what std::shared_ptr
does is define template converting constructors:
namespace std {
template <class T>
class shared_ptr {
public:
template <class Y>
shared_ptr(const shared_ptr<Y>&);
template <class Y>
shared_ptr(shared_ptr<Y>&&);
// ...
};
}
Though the declaration as shown would allow conversions from any shared_ptr
to any other, not just when the template argument types are compatible. But the Standard also says about these constructors ([util.smartptr]/5 and [util.smartptr.const]/18 and util.smartptr.const]/21):
For the purposes of subclause [util.smartptr], a pointer type
Y*
is said to be compatible with a pointer typeT*
when eitherY*
is convertible toT*
orY
isU[N]
andT
is cvU[]
.The [...] constructor shall not participate in overload resolution unless
Y*
is compatible withT*
.
Although this restriction could be done in any way, including compiler-specific features, most implementations will enforce the restriction using an SFINAE technique (Substitution Failure Is Not An Error). One possible implementation:
#include <cstddef>
#include <type_traits>
namespace std {
template <class Y, class T>
struct __smartptr_compatible
: is_convertible<Y*, T*> {};
template <class U, class V, size_t N>
struct __smartptr_compatible<U[N], V[]>
: bool_constant<is_same_v<remove_cv_t<U>, remove_cv_t<V>> &&
is_convertible_v<U*, V*>> {};
template <class T>
class shared_ptr {
public:
template <class Y, class = enable_if_t<__smartptr_compatible<Y, T>::value>>
shared_ptr(const shared_ptr<Y>&);
template <class Y, class = enable_if_t<__smartptr_compatible<Y, T>::value>>
shared_ptr(shared_ptr<Y>&&);
// ...
};
}
Here the helper template __smartptr_compatible<Y, T>
acts as a "trait": it has a static constexpr
member value
which is true
when the types are compatible as defined, or false
otherwise. Then std::enable_if
is a trait which has a member type called type
when its first template argument is true
, or does not have a member named type
when its first template argument is false
, making the type alias std::enable_if_t
invalid.
So if template type deduction for either constructor deduces the type Y
so that Y*
is not compatible with T*
, substituting that Y
into the enable_if_t
default template argument is invalid. Since that happens while substituting a deduced template argument, the effect is just to remove the entire function template from consideration for overload resolution. Sometimes an SFINAE technique is used to force selecting a different overload instead, or as here (most of the time), it can just make the user's code fail to compile. Though in the case of the compile error, it will help that a message appears somewhere in the output that the template was invalid, rather than some error even deeper within internal template code. (Also, an SFINAE setup like this makes it possible for a different template to use its own SFINAE technique to test whether or not a certain template specialization, type-dependent expression, etc. is or isn't valid.)
Covariant virtual functions and smart pointers
Is it possible to expand this feature to smart pointers as well? (Assuming a smart pointer is some template class)
No: C++ doesn't know/allow covariant or contravariant templates. There's no relation between types Ptr<A>
and Ptr<B>
, even if A
inherits from B
.
C++ : Covariant return type without pointer
The idea of a covariant return type is a polymorpihc return type. And in C++, you can't have run time polymorphism without pointers or references. Let's ignore for a second most of the hardships, and pretend it's possible. Here is my code that handles things by your Parent
interface:
void bar(Parent * p) {
auto o = p->foo();
}
What is o
? Well, it's Parent
of course. Says so in Parent::foo
's return type. But what if that p
is pointing at a Child
? The deduced type of o
is still Parent
, so at best I get a sliced object. No polymorphic behavior, so the whole exercise is pointless.
At worst, and quite likely, I get undefined behavior.
That's why co-variant return types have to be pointers or references.
Covariant data types: Why return type must be same as or child of its 'parent method' return type?
The way to understand this is to think of the subclass as a specific type of the parent class. This means it still needs to adhere to the behavior defined by the parent. The parent class defines a someMethod
method that returns an Object
. Subclasses can't break this behavior, but they can further specify it - a DogHouse
's someMethod
still returns an Object
, it just happens to be a String
.
How to deal with not covariant errors in paired hierarchies (C++)
We, unfortunately, cannot forward declare classes WITH inheritance.
So you have to drop one covariant return type and break the circular dependency loop somewhere.
Related Topics
Why Is Statically Linking Glibc Discouraged
How to Reduce Compile Time, and Linking Time for Visual C++ Projects (Native C++)
Difference Between Using #Include<Filename> and #Include<Filename.H> in C++
Casting Pointer to Array (Int* to Int[2])
Getting the MAChine Serial Number and CPU Id Using C/C++ in Linux
How to Define a String Literal in Gcc Command Line
What Exactly Is an 'Aligned Pointer'
Is There Any Lame C++ Wrapper\Simplifier (Working on Linux MAC and Win from Pure Code)
Program Behaving Strangely on Online Ides
Determining Exception Type After the Exception Is Caught
Does New[] Call Default Constructor in C++
Popen Simultaneous Read and Write
C++11 Way to Index Tuple at Runtime Without Using Switch
Initializing Default Values in a Struct
How to Provider User with Autocomplete Suggestions for Given Boost::Spirit Grammar
Why Can't I Open Avi Video in Opencv
How to Set Breakpoints on Future Shared Libraries with a Command Flag