C++ 协变 return 类型的缺点是什么?

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

我最近不得不处理 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_caststatic_cast.

这种方法有什么缺点?这是设计不良的标志吗?

谢谢。

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

切勿通过原始指针 (T*) 或引用 (T&) 转移所有权 的 C++ 核心指南。有一些技巧可以限制这个问题,但协变值仍然是一个原始指针。

文档中的示例:

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

编辑

展示了一种模拟语言协变的技术,以及一些额外的代码。它有一些潜在的维护成本,所以在决定走哪条路之前要考虑到这一点。

在 C++ 中实现的协变 return 类型的主要限制是它们仅适用于原始指针和引用。没有真正的理由尽可能使用它们,但限制意味着我们不能总是在需要它们时使用它们。

很容易克服这个限制,同时提供相同的用户体验,而无需依赖语言功能。方法如下。

让我们使用常见且流行的 non-virtual interface 习语重写我们的 classes。

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 不再是虚拟的,因此可以有任何 return 类型。它不受协变 return 类型规则的约束。 create_impl 仍然受到约束,但它是私有的,除了 class 本身没有人调用它,因此我们可以轻松地操纵它并完全消除协方差。

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 都具有与以前完全相同的接口,看不到协变 return 类型。这对我们意味着什么?这意味着我们可以自由使用智能指针。

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

在这里,我们克服了语言限制,并为我们的 classes 提供了等价的协变 return 类型,而不依赖于有限的语言功能。

注意技术倾向。

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

这里的函数隐式地将 ("upcasts") Derived 指针转换为 Base 指针。如果我们使用语言提供的协变 return 类型,编译器会在需要时自动插入此类向上转换。不幸的是,该语言的智能程度只能用于原始指针。对于其他一切,我们必须自己做。幸运的是,它相对容易,即使有点冗长。

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