简体   繁体   English

C++ 中的协变返回类型究竟是什么?

[英]What exactly are covariant return types in C++?

I get a compile error when I try to do this:尝试执行此操作时出现编译错误:

class A
{
    virtual std::vector<A*> test() { /* do something */ };
}

class B: public A
{
    virtual std::vector<B*> test() { /* do something */ };
}

I assume that A and B are covariant types, and hence A* and B* should also be (Correct?) By inference, I would have expected that std::vector<A*> and std::vector<B*> should be covariant as well, but this does not seem to be the case.我假设 A 和 B 是协变类型,因此 A* 和 B* 也应该是(正确的?)通过推断,我本以为std::vector<A*>std::vector<B*>应该也是协变的,但情况似乎并非如此。 Why?为什么?

Covariant return types allow overridden virtual member functions in a derived class to return a different type of object, as long as it can be used in all the same ways as the base class's return type.协变返回类型允许派生类中的重写虚拟成员函数返回不同类型的对象,只要它可以以与基类的返回类型相同的方式使用。 Computer scientists have (ever since Barbara Liskov) a theoretical definition of "can be used in the same ways": substitutability .计算机科学家(自 Barbara Liskov 起)对“可以以相同方式使用”的理论定义为:可替代性

No, std::vector<B*> is not a subtype of std::vector<A*> , nor should it be.不, std::vector<B*>不是std::vector<A*>的子类型,也不应该是。

For example, std::vector<B*> doesn't support the push_back(A*) operation, so it is not substitutable.例如, std::vector<B*>不支持push_back(A*)操作,因此它不可替代。

C++ doesn't try to infer subtype relationships for templates at all. C++ 根本不尝试推断模板的子类型关系。 The relationship will only exist if you actually specialize one and specify a base class.只有当您真正专门化一个并指定一个基类时,这种关系才会存在。 One reason for this, even on interfaces which are theoretically covariant (basically, read-only), is that C++'s version is actually stronger than Liskov substitution -- in C++ the compatibility has to exist at a binary level.这样做的一个原因,即使在理论上是协变的(基本上是只读的)接口上,也是 C++ 的版本实际上比 Liskov 替换更强——在 C++ 中,兼容性必须存在于二进制级别。 Since the memory layout of collections of related objects may not match subobject placement, this binary compatibility isn't achieved.由于相关对象集合的内存布局可能与子对象放置不匹配,因此无法实现这种二进制兼容性。 The restriction of covariant return types to be only pointers or references is also a consequence of the binary compatibility issue.协变返回类型仅限于指针或引用的限制也是二进制兼容性问题的结果。 A derived object probably wouldn't fit in the space reserved for the base instance... but its pointer will.派生对象可能不适合为基本实例保留的空间......但它的指针会。

An apple is a fruit.苹果是一种水果。

A bag of apples is not a bag of fruit.一袋苹果不是一袋水果。 That's because you can put a pear in a bag of fruit.那是因为你可以把梨放在一袋水果里。

The C++ FAQ answers this directly in [21.3] Is a parking-lot-of-Car a kind-of parking-lot-of-Vehicle? C++ FAQ 在[21.3] 中直接回答了这个问题 ("You don't have to like it. But you do have to accept it.") (“你不必喜欢它。但你必须接受它。”)

SO question Getting a vector into a function that expects a vector is asking the same thing. SO question 将向量放入一个期望向量的函数中是在问同样的事情。 And the answer is that while it seems safe at first to allow covariance of generic types, in particular containers of a derived type being treated as containers of the base type, it is quite unsafe.答案是,虽然起初允许泛型类型的协变似乎是安全的,特别是派生类型的容器被视为基类型的容器,但它是非常不安全的。

Consider this code:考虑这个代码:

class Vehicle {};
class Car : public Vehicle {};
class Boat : public Vehicle {};

void add_boat(vector<Vehicle*>& vehicles) { vehicles.push_back(new Boat()); }

int main()
{
  vector<Car*> cars;
  add_boat(cars);
  // Uh oh, if that worked we now have a Boat in our Cars vector.
  // Fortunately it is not legal to convert vector<Car*> as a vector<Vehicle*> in C++.
}

The standard defines covariance for C++ purposes in §10.3 [class.virtual]/p7:该标准在 §10.3 [class.virtual]/p7 中定义了用于 C++ 目的的协方差:

The return type of an overriding function shall be either identical to the return type of the overridden function or covariant with the classes of the functions.覆盖函数的返回类型应与被覆盖函数的返回类型相同或与函数的类协变 If a function D::f overrides a function B::f , the return types of the functions are covariant if they satisfy the following criteria:如果函数D::f覆盖函数B::f ,则如果函数满足以下条件,则函数的返回类型是协变的:

  • both are pointers to classes, both are lvalue references to classes, or both are rvalue references to classes 113两者都是类的指针,都是类的左值引用,或者都是类的右值引用113
  • the class in the return type of B::f is the same class as the class in the return type of D::f , or is an unambiguous and accessible direct or indirect base class of the class in the return type of D::f在返回类型的类B::f是相同的类中的返回类型的类D::f ,或者是一个明确的和可访问的直接或间接的基类中的返回类型的类的D::f
  • both pointers or references have the same cv-qualification and the class type in the return type of D::f has the same cv-qualification as or less cv-qualification than the class type in the return type of B::f .指针或引用都具有相同的 cv 限定,并且D::f返回类型中的类类型具有与B::f返回类型中的类类型相同或更少的 cv 限定。

113 Multi-level pointers to classes or references to multi-level pointers to classes are not allowed. 113 类的多级指针或对类的多级指针的引用是不允许的。

Your functions fail on the first point, and, even if you bypass it, fails on the second - std::vector<A*> is not a base of std::vector<B*> .你的函数在第一点失败,即使你绕过它,在第二点失败 - std::vector<A*>不是std::vector<B*>

Templates do not "inherit" covariance, because different template specializations may be completely 100% unrelated:模板不会“继承”协方差,因为不同的模板特化可能完全 100% 不相关:

template<class T> struct MD;

//pets
template<> struct MD<A*> 
{
    std::string pet_name;
    int pet_height;
    int pet_weight;
    std::string pet_owner;
};

//vehicles
template<> struct MD<B*>
{
    virtual ~MD() {}
    virtual void fix_motor();
    virtual void drive();
    virtual bool is_in_the_shop()const;
}

std::vector<MD<A*>> get_pets();

How would you feel if get_pets returned a vector where some of those were actually vehicles instead?如果get_pets返回一个向量,其中一些实际上是车辆,你会有什么感觉? It seems to defeat the point of the type system right?它似乎打败了类型系统的要点,对吗?

Covariance only happens when you are returning a pointer or a reference to a class, and the classes are related by inheritance.协方差仅在您返回对类的指针或引用时发生,并且这些类通过继承相关联。

This is clearly not happening, both because std::vector<?> is not a pointer nor reference, and because two std::vector<?> s have no parent/child relationship.这显然不会发生,因为std::vector<?>既不是指针也不是引用,也因为两个std::vector<?>没有父/子关系。

Now, we can make this work.现在,我们可以完成这项工作。

Step 1, create an array_view class.第 1 步,创建一个array_view类。 It has a begin and end pointer and methods and a size method and all you might expect.它有一个beginend指针和方法以及一个size方法以及您可能期望的所有内容。

Step 2, create an shared_array_view , which is an array view that also owns a shared_ptr<void> with a custom deleter: it is otherwise identical.第 2 步,创建一个shared_array_view ,它是一个数组视图,它也拥有一个带有自定义删除器的shared_ptr<void> :它在其他方面是相同的。 This class also insures that the data it is viewing lasts long enough to be viewed.此类还确保它正在查看的数据持续足够长的时间以供查看。

Step 3, create a range_view , which is a pair of iterators and dressing on it.第 3 步,创建一个range_view ,它是一对迭代器并在其上进行修整。 Do the same with a shared_range_view with an ownership token.对带有所有权令牌的shared_range_view执行相同的操作。 Modify your array_view to be a range_view with some extra guarantees (contiguous iterators mainly).将您的array_view修改为具有一些额外保证的range_view (主要是连续迭代器)。

Step 4, write a converting iterator.第四步,编写一个转换迭代器。 This is a type that stores an iterator over value_type_1 which either calls a function, or implicitly converts to a const_iterator over value_type_2 .这是一种类型,其存储在一个迭代value_type_1其中或者调用一个函数,或隐式转换为常量性超过value_type_2

Step 5, Write a range_view< implicit_converting_iterator< T*, U* > > returning function for when T* can be implicitly converted to U* .第 5 步,编写一个range_view< implicit_converting_iterator< T*, U* > >返回函数,用于何时T*可以隐式转换为U*

Step 6, write type erasers for the above第六步,为上面写类型橡皮擦

class A {
  owning_array_view<A*> test_() { /* do something */ }
  virtual type_erased_range_view<A*> test() { return test_(); };
};

class B: public A {
  owning_array_view<B*> test_() { /* do something */ };
  virtual type_erased_range_view<A*> test() override {
    return convert_range_to<A*>(test_());
  }
};

Most of what I describe has been done by boost.我所描述的大部分内容都是由 boost 完成的。

This doesn't work because这不起作用,因为

  1. you are not returning pointers or references, which is required for covariant returns to work;您没有返回指针或引用,这是协变返回工作所必需的; and
  2. Foo<B> and Foo<B> have no inheritance relationship regardless of Foo , A and B (unless there's a specialization that makes it so). Foo<B>Foo<B>没有继承关系,无论FooAB (除非有特殊化,否则)。

But we can work around that.但我们可以解决这个问题。 First, note that std::vector<A*> and std::vector<B*> are not substitutable for each other, regardless of any language restrictions, simply because std::vector<B*> cannot support adding an A* element to it.首先,请注意std::vector<A*>std::vector<B*>不能相互替代,无论任何语言限制如何,仅仅因为std::vector<B*>不支持添加A*元素。 So you cannot even write a custom adapter that makes std::vector<B*> a substitute of std::vector<A*>因此,您甚至无法编写使std::vector<B*>替代std::vector<A*>的自定义适配器

But a read-only container of B* can be adapted to look like a read-only container of A* .但是B*只读容器可以调整为看起来像A*的只读容器。 This is a multi-step process.这是一个多步骤的过程。

Create an abstract class template that exports a readonly container-like interface创建一个抽象类模板,导出一个只读的类容器接口

template <class ApparentElemType>
struct readonly_vector_view_base
{
    struct iter
    {
        virtual std::unique_ptr<iter> clone() const = 0;

        virtual ApparentElemType operator*() const = 0;
        virtual iter& operator++() = 0;
        virtual iter& operator--() = 0;
        virtual bool operator== (const iter& other) const = 0;
        virtual bool operator!= (const iter& other) const = 0;
        virtual ~iter(){}
    };

    virtual std::unique_ptr<iter> begin() = 0;
    virtual std::unique_ptr<iter> end() = 0;

    virtual ~readonly_vector_view_base() {}
};

It return pointers to iterators, not iterators themselves, but don't worry, this class will be only used by an STL-like wrapper anyway.它返回指向迭代器的指针,而不是迭代器本身,但别担心,这个类只会被类 STL 的包装器使用。

Now create a concrete wrapper for readonly_vector_view_base and its iterator, so that it contains a pointer to, and delegate its operations to, a readonly_vector_view_base .现在为readonly_vector_view_base及其迭代器创建一个具体的包装器,以便它包含一个指向readonly_vector_view_base的指针,并将其操作委托给一个readonly_vector_view_base

template <class ApparentElemType>
class readonly_vector_view
{
  public:
    readonly_vector_view(const readonly_vector_view& other) : pimpl(other.pimpl) {}
    readonly_vector_view(std::shared_ptr<readonly_vector_view_base<ApparentElemType>> pimpl_) : pimpl(pimpl_) {}

    typedef typename readonly_vector_view_base<ApparentElemType>::iter iter_base;
    class iter
    {
      public:
        iter(std::unique_ptr<iter_base> it_) : it(it_->clone()) {}
        iter(const iter& other) : it(other.it->clone()) {}
        iter& operator=(iter& other) { it = other.it->clone(); return *this; }

        ApparentElemType operator*() const { return **it; }

        iter& operator++() { ++*it; return *this; }
        iter& operator--() { --*it; return *this; }
        iter operator++(int) { iter n(*this); ++*it; return n; }
        iter operator--(int) { iter n(*this); --*it; return n; }

        bool operator== (const iter& other) const { return *it == *other.it; }
        bool operator!= (const iter& other) const { return *it != *other.it; }
      private:
        std::unique_ptr<iter_base> it;
    };

    iter begin() { return iter(pimpl->begin()); }
    iter end() { return iter(pimpl->end()); }
  private:
    std::shared_ptr<readonly_vector_view_base<ApparentElemType>> pimpl;
};

Now create a templatized implementation for readonly_vector_view_base that looks at a vector of a differently typed elements:现在为readonly_vector_view_base创建一个模板化实现,它查看不同类型元素的向量:

template <class ElemType, class ApparentElemType>
struct readonly_vector_view_impl : readonly_vector_view_base<ApparentElemType>
{
    typedef typename readonly_vector_view_base<ApparentElemType>::iter iter_base;

    readonly_vector_view_impl(std::shared_ptr<std::vector<ElemType>> vec_) : vec(vec_) {}

    struct iter : iter_base
    {
        std::unique_ptr<iter_base> clone() const { std::unique_ptr<iter_base> x(new iter(it)); return x; }

        iter(typename std::vector<ElemType>::iterator it_) : it(it_) {}

        ApparentElemType operator*() const { return *it; }

        iter& operator++() { ++it; return *this; }
        iter& operator--() { ++it; return *this; }

        bool operator== (const iter_base& other) const {
            const iter* real_other = dynamic_cast<const iter*>(&other);
            return (real_other && it == real_other->it);
        }
        bool operator!= (const iter_base& other) const { return ! (*this == other); }

        typename std::vector<ElemType>::iterator it;
    };

    std::unique_ptr<iter_base> begin() {
        iter* x (new iter(vec->begin()));
        std::unique_ptr<iter_base> y(x);
        return y;
    }
    std::unique_ptr<iter_base> end() {
        iter* x (new iter(vec->end()));;
        std::unique_ptr<iter_base> y(x);
        return y;
    }

    std::shared_ptr<std::vector<ElemType>> vec;
};

OK, as long as we have two types where one is convertible to another, such as A* and B* , we can view a vector of B* as if it's a vector of A* .好的,只要我们有两种类型,其中一种可以转换为另一种,例如A*B* ,我们就可以将B*的向量视为A*的向量。

But what does it buy us?但它给我们带来了什么? readonly_vector_view<A*> is still unrelated to readonly_vector_view<B*> ! readonly_vector_view<A*>仍然与readonly_vector_view<B*>无关! Read on...继续阅读...

It turns out that the covariant return types are not really necessary, they are a syntactic sugar to what is available in C++ otherwise.事实证明,协变返回类型并不是真正必要的,它们是 C++ 中可用内容的语法糖。 Suppose C++ doesn't have covariant return types, can we simulate them?假设 C++ 没有协变返回类型,我们可以模拟它们吗? It's actually pretty easy:其实很简单:

class Base
{
   virtual Base* clone_Base() { ... actual impl ... }
   Base* clone() { return clone_Base(); } // note not virtual 
};

class Derived : public Base
{
   virtual Derived* clone_Derived() { ... actual impl ... }
   virtual Base* clone_Base() { return clone_Derived(); }
   Derived* clone() { return clone_Derived(); } // note not virtual 

};

It's actually pretty easy and there's no requirement for the return type to be pointers or references, or have an inheritance relationship .这实际上很简单,并且没有要求返回类型是指针或引用,或具有继承关系 It is enough that there is a conversion:有一个转换就足够了:

class Base
{
   virtual shared_ptr<Base> clone_Base() { ... actual impl ... }
   shared_ptr<Base> clone() { return clone_Base(); } 
};

class Derived : public Base
{
   virtual shared_ptr<Derived> clone_Derived() { ... actual impl ... }
   virtual shared_ptr<Base> clone_Base() { return clone_Derived(); }
   shared_ptr<Derived> clone() { return clone_Derived(); } 
};

In a similar fashion, we can arrange A::test() to return a readonly_vector_view<A*> , and B::test() to return a readonly_vector_view<B*> .以类似的方式,我们可以安排A::test()返回一个readonly_vector_view<A*> ,和B::test()返回一个readonly_vector_view<B*> Since these functions are now not virtual, there is no requirement for their return types to be in any relationship.由于这些函数现在不是虚拟的,因此不需要它们的返回类型有任何关系。 One just hides the other.一个只是隐藏另一个。 But inside they call a virtual function that creates (say) a readonly_vector_view<A*> implemented in terms of readonly_vector_view_impl<B*, A*> which is implemented in terms of vector<B*> , and everything works just as if they were real covariant return types.但是在内部,他们调用了一个虚函数,该函数创建(比如说)一个readonly_vector_view<A*>实现了readonly_vector_view_impl<B*, A*> ,它是根据vector<B*> ,一切都像它们一样工作真正的协变返回类型。

struct A
{
    readonly_vector_view<A*> test() { return test_A(); }
    virtual readonly_vector_view<A*> test_A() = 0;
};

struct B : A
{
    std::shared_ptr<std::vector<B*>> bvec;

    readonly_vector_view<B*> test() { return test_B(); }

    virtual readonly_vector_view<A*> test_A() {
        return readonly_vector_view<A*>(std::make_shared<readonly_vector_view_impl<B*, A*>>(bvec));
    }
    virtual readonly_vector_view<B*> test_B() {
        return readonly_vector_view<B*>(std::make_shared<readonly_vector_view_impl<B*, B*>>(bvec));
    }
};

Piece of cake!小菜一碟! Live demo Totally worth the effort!现场演示完全值得付出努力!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM