I have recently had to deal with C++ covariance return types such as the following construct :
struct Base
{
virtual ~Base();
};
struct Derived : public Base {};
struct AbstractFactory
{
virtual Base *create() = 0;
virtual ~AbstractFactory();
};
struct ConcreteFactory : public AbstractFactory
{
virtual Derived *create()
{
return new Derived;
}
};
It allows the client code to treat the Derived
object as a Base
type or as a Derived
type when needed and especially without the use of dynamic_cast
or static_cast
.
What are the drawbacks of this approach ? Is it a sign of bad design ?
Thank you.
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.)