What are the drawbacks of C++ covariance return types?

最后都变了- 提交于 2021-02-20 03:49:43

问题


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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!