问题
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.
回答1:
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.)
回答2:
Covariance does not work for smart pointers, and as such covariance violates:
Never transfer ownership by a raw pointer (T*) or reference (T&) of the C++ Core Guidelines. There are tricks to limit the issue, but still the covariant value is a raw pointer.
An example from the document:
X* compute(args) // don't
{
X* res = new X{};
// ...
return res;
}
This is almost the same as what the code in the question is doing:
virtual Derived *create()
{
return new Derived;
}
Unfortunately the following is illegal, both for shared_ptr
and for unique_ptr
:
struct AbstractFactory
{
virtual std::shared_ptr<Base> create() = 0;
};
struct ConcreteFactory : public AbstractFactory
{
/*
<source>:16:38: error: invalid covariant return type for 'virtual std::shared_ptr<Derived> ConcreteFactory::create()'
*/
virtual std::shared_ptr<Derived> create()
{
return std::make_shared<Derived>();
}
};
EDIT
n.m.'s answer shows a technique that simulates language covariance, with some additional code. It has some potential maintenance costs, so take that into account before deciding which way to go.
来源:https://stackoverflow.com/questions/54439944/what-are-the-drawbacks-of-c-covariance-return-types