【问题标题】:What exactly are covariant return types in C++?C++ 中的协变返回类型到底是什么?
【发布时间】:2014-11-06 22:28:46
【问题描述】:

尝试执行此操作时出现编译错误:

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

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

我假设 A 和 B 是协变类型,因此 A* 和 B* 也应该是(正确的?)通过推论,我原以为 std::vector&lt;A*&gt;std::vector&lt;B*&gt; 也应该是协变的,但这似乎并非如此。为什么?

【问题讨论】:

  • 我猜这将允许B b = ...; vector&lt;A*&gt; x = b.test();,现在我们可以将A* 对象添加到Bs 的向量中,这打破了vector&lt;B*&gt; 的保证。但是我对 C++ 的了解非常有限。
  • C++, polymorphism vs. templatization of a function argument 的可能重复项。嗯,那里的答案之一 与这里的答案之一完全相同。

标签: c++ inheritance vector virtual covariant


【解决方案1】:

模板不会“继承”协方差,因为不同的模板特化可能完全 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();

如果get_pets 返回一个向量,其中一些实际上是车辆,你会有什么感觉?这似乎打败了类型系统的要点吧?

【讨论】:

  • 可写集合不是协变的,无论专业化如何。
【解决方案2】:

协变返回类型允许派生类中重写的虚成员函数返回不同类型的对象,只要它可以以与基类的返回类型相同的方式使用。计算机科学家(自 Barbara Liskov 以来)对“可以以相同方式使用”有一个理论定义:可替代性

不,std::vector&lt;B*&gt; 不是std::vector&lt;A*&gt; 的子类型,也不应该是。

例如std::vector&lt;B*&gt;不支持push_back(A*)操作,所以不可替代。

C++ 根本不尝试推断模板的子类型关系。只有当您真正专门化一个并指定一个基类时,这种关系才会存在。造成这种情况的一个原因是,即使在理论上是协变的(基本上是只读的)接口上,C++ 的版本实际上也比 Liskov 替换更强——在 C++ 中,兼容性必须存在于二进制级别。由于相关对象集合的内存布局可能与子对象放置不匹配,因此无法实现这种二进制兼容性。将协变返回类型限制为仅是指针或引用也是二进制兼容性问题的结果。派生对象可能不适合为基本实例保留的空间......但它的指针会。

【讨论】:

  • re “将协变返回类型限制为仅是指针或引用也是二进制兼容性问题的结果。”表面上听起来很可能。但这不是一个关键的论点。考虑到实现对虚函数所需结果存储大小的检查是微不足道的(对于编译器)。这将是一种实施的成本。由于不为不使用的东西付费的 C++ 原则,因此必须将通用协变函数标记为此类。那么我们就要考虑成本、语言复杂性、执行时间、代码大小和收益。
  • 还有另一个考虑因素,即通常的“手动”实现协方差,例如作为函数结果的智能指针,依赖于计算转换而不是基类关系。因此,基于继承的协变结果的 C++ 功能对智能指针没有帮助,即对似乎最常见的情况没有帮助。 :(
  • @Alf:如果大小问题是二进制兼容性的唯一含义,那么作为扩展,一个特定的实现可以允许这样做。调用正确的析构函数将是按值协变返回的另一个问题。
【解决方案3】:

苹果是一种水果。

一袋苹果不是一袋水果。那是因为你可以把一个梨放在一袋水果里。

【讨论】:

  • 你可以把一个梨放在一袋苹果里,你不应该把香蕉和其他水果混在一起。 :-P
  • 你当然是指梨柄。否则你可能会吃到梨片。
  • @Kay 除了草莓...香蕉和草莓切片,加一点柠檬汁和糖...好吃!
  • @BenVoigt 奇怪的是,我的厨房里碰巧有一袋水果,而我刚刚摘了一个苹果和一个梨。据我所知,里面没有切片或手柄。
  • @Kay:这让我想起了老格劳乔·马克思所看到的:时间如箭,果蝇如香蕉。
【解决方案4】:

C++ FAQ 直接在[21.3] Is a parking-lot-of-Car a kind-of parking-lot-of-Vehicle? 中回答了这个问题(“你不必喜欢它。但你必须接受它。”)

所以问题Getting a vector into a function that expects a vector 在问同样的事情。答案是,虽然一开始允许泛型类型的协变似乎是安全的,特别是派生类型的容器被视为基类型的容器,但它是非常不安全的。

考虑这段代码:

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++.
}

【讨论】:

    【解决方案5】:

    标准在 §10.3 [class.virtual]/p7 中为 C++ 目的定义协方差:

    重写函数的返回类型应与 被覆盖函数或 协变 的返回类型 函数的类。如果函数 D::f 覆盖了函数 B::f,函数的返回类型是协变的,如果它们 满足以下条件:

    • 都是类的指针,都是类的左值引用,或者都是类的右值引用113
    • B::f 的返回类型中的类与D::f 的返回类型中的类是同一类,或者是一个明确且 返回中类的可访问的直接或间接基类 D::f 的类型
    • 指针或引用都具有相同的 cv 限定,并且D::f 的返回类型中的类类型具有相同的 cv 限定 与返回类型中的类类型相同或更少的 cv 限定 B::f

    113不允许多级指向类的指针或对指向类的多级指针的引用。

    您的函数在第一点失败,即使您绕过它,第二点也会失败 - std::vector&lt;A*&gt; 不是 std::vector&lt;B*&gt; 的基础。

    【讨论】:

    • 第一点很容易解决。更大的问题是第二个。
    【解决方案6】:

    只有在返回类的指针或引用时才会发生协变,并且类通过继承相关。

    这显然不会发生,既因为std::vector&lt;?&gt; 不是指针也不是引用,也因为两个std::vector&lt;?&gt;s 没有父/子关系。

    现在,我们可以完成这项工作了。

    第 1 步,创建一个 array_view 类。它有一个beginend 指针和方法以及一个size 方法和你所期望的一切。

    第 2 步,创建一个 shared_array_view,这是一个数组视图,它还拥有一个带有自定义删除器的 shared_ptr&lt;void&gt;:其他方面是相同的。此类还确保它正在查看的数据持续足够长的时间以供查看。

    第三步,创建一个range_view,它是一对迭代器,并在其上进行修饰。对带有所有权令牌的shared_range_view 执行相同操作。将您的 array_view 修改为 range_view 并提供一些额外的保证(主要是连续迭代器)。

    第四步,编写一个转换迭代器。这是一种在 value_type_1 上存储迭代器的类型,该迭代器要么调用函数,要么隐式转换为 value_type_2 上的 const_iterator。

    第五步,写一个range_view&lt; implicit_converting_iterator&lt; T*, U* &gt; &gt;返回函数,当T*可以隐式转换为U*

    第6步,为上面写类型橡皮擦

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

    我描述的大部分内容都是通过 boost 完成的。

    【讨论】:

    • 这里没有协方差。 b.test() 的用户直接看不到比(A&amp;)b 的用户更具体的返回类型
    • 唉,A::testB::test 的返回 types 相同。
    【解决方案7】:

    这不起作用,因为

    1. 您没有返回指针或引用,这是协变返回工作所必需的;和
    2. 无论FooAB 是什么,Foo&lt;B&gt;Foo&lt;B&gt; 都没有继承关系(除非有专门化的原因)。

    但我们可以解决这个问题。首先,请注意std::vector&lt;A*&gt;std::vector&lt;B*&gt; 不能相互替代,无论任何语言限制,仅仅是因为std::vector&lt;B*&gt; 不支持向其添加A* 元素。所以你甚至不能编写一个自定义适配器来让std::vector&lt;B*&gt; 替代std::vector&lt;A*&gt;

    但是B*只读 容器可以修改为看起来像A* 的只读容器。这是一个多步骤的过程。

    创建一个抽象类模板,导出一个只读的类似容器的接口

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

    它返回指向迭代器的指针,而不是迭代器本身,而是 别担心,无论如何,这个类只会被类似 STL 的包装器使用。

    现在为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;
    };
    

    现在为 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;
    };
    

    好的,只要我们有两种可以转换为另一种的类型,例如A*B*,我们就可以查看B*的向量,就好像它是A*的向量一样。

    但它给我们带来了什么? readonly_vector_view&lt;A*&gt; 仍然与 readonly_vector_view&lt;B*&gt; 无关!继续阅读...

    事实证明,协变返回类型并不是真正必要的,它们是 C++ 中可用的语法糖。假设 C++ 没有协变返回类型,我们可以模拟它们吗?其实很简单:

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

    其实很简单而且返回类型不要求是指针或引用,也不要求有继承关系。有一个转换就足够了:

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

    以类似的方式,我们可以安排A::test() 返回readonly_vector_view&lt;A*&gt;,并安排B::test() 返回readonly_vector_view&lt;B*&gt;。由于这些函数现在不是虚拟的,因此不需要它们的返回类型有任何关系。一个只是隐藏另一个。但在内部,他们调用了一个虚函数,该函数创建(比如说)一个根据 readonly_vector_view_impl&lt;B*, A*&gt; 实现的 readonly_vector_view&lt;A*&gt;,它是根据 vector&lt;B*&gt; 实现的,并且一切都像真正的协变返回类型一样工作。

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

    小菜一碟! Live demo 完全值得努力!

    【讨论】:

    • 这不是vector_view的完整实现,只是一个概念证明。一个完整的实现会长很多倍。
    • 不知道为什么要编辑,但编辑不顺利,有些代码丢失了,有些代码被格式化了。
    猜你喜欢
    • 1970-01-01
    • 2011-11-25
    • 2015-12-08
    • 2011-02-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-02
    • 1970-01-01
    相关资源
    最近更新 更多