C ++协方差返回类型的缺点是什么?

时间:2019-01-30 11:47:55

标签: c++ covariance

我最近不得不处理C ++ covariance return types,例如以下结构:

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;
    }
};

它允许客户端代码在需要时将Derived对象视为Base类型或Derived类型,尤其是无需使用dynamic_cast或{{ 1}}。

此方法的缺点是什么?这是不良设计的征兆吗?

谢谢。

2 个答案:

答案 0 :(得分:5)

协方差不适用于智能指针,因此协方差违反了:

《 C ++核心准则》的

Never transfer ownership by a raw pointer (T*) or reference (T&) 。有一些技巧可以限制问题,但协变量值仍是原始指针。

文档中的示例:

X* compute(args)    // don't
{
    X* res = new X{};
    // ...
    return res;
}

这与问题中的代码几乎相同:

virtual Derived *create()
{
    return new Derived;
}

不幸的是,对于shared_ptrunique_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>();
    }
};

编辑

n.m.'s answer显示了一种使用一些附加代码来模拟语言协方差的技术。它有一些潜在的维护成本,因此在决定走哪条路之前要考虑到这一点。

答案 1 :(得分:2)

在C ++中实现的协变返回类型的主要限制是它们只能与原始指针和引用一起使用。没有真正的理由在可能的情况下使用它们,但是这种限制意味着我们不能在需要它们时始终使用它们。

很容易克服此限制,同时提供相同的用户体验,而无需依赖语言功能。就是这样。

让我们使用常见且流行的non-virtual interface惯用法重写我们的类。

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;
    }
};

现在,这里发生了一些有趣的事情。 create不再是虚拟的,因此可以具有任何返回类型。它不受协变量返回类型规则的约束。 create_impl仍然受到限制,但是它是私有的,除了类本身之外,没有人调用它,因此我们可以轻松地对其进行操作并完全消除协方差。

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;
    }
};

现在AbstractFactoryConcreteFactory都具有与以前完全相同的接口,并且看不到协变返回类型。对我们意味着什么?这意味着我们可以自由使用智能指针。

// 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>();
    }
};

在这里,我们克服了语言限制,并为类提供了等效的协变返回类型,而不必依赖有限的语言功能。

注意技术倾向。

    sptr<Base> create_impl() override {
        return create_impl_derived();
    }

此函数在此将Derived指针隐式转换(“转换”)为Base指针。如果我们使用该语言提供的协变返回类型,则编译器会在需要时自动插入这种向上转换。不幸的是,该语言仅够聪明,足以用于原始指针。对于其他所有事情,我们必须自己做。幸运的是,如果有点冗长,这相对容易。

(在这种特殊情况下,只返回一个Base指针是 是可以接受的。我不讨论这个。我假设我们绝对需要像协变返回类型这样的东西。 )