繁体   English   中英

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

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

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

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<A*>std::vector<B*>应该也是协变的,但情况似乎并非如此。 为什么?

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

不, std::vector<B*>不是std::vector<A*>的子类型,也不应该是。

例如, std::vector<B*>不支持push_back(A*)操作,因此它不可替代。

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

苹果是一种水果。

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

C++ FAQ 在[21.3] 中直接回答了这个问题 (“你不必喜欢它。但你必须接受它。”)

SO question 将向量放入一个期望向量的函数中是在问同样的事情。 答案是,虽然起初允许泛型类型的协变似乎是安全的,特别是派生类型的容器被视为基类型的容器,但它是非常不安全的。

考虑这个代码:

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

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

覆盖函数的返回类型应与被覆盖函数的返回类型相同或与函数的类协变 如果函数D::f覆盖函数B::f ,则如果函数满足以下条件,则函数的返回类型是协变的:

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

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

你的函数在第一点失败,即使你绕过它,在第二点失败 - std::vector<A*>不是std::vector<B*>

模板不会“继承”协方差,因为不同的模板特化可能完全 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返回一个向量,其中一些实际上是车辆,你会有什么感觉? 它似乎打败了类型系统的要点,对吗?

协方差仅在您返回对类的指针或引用时发生,并且这些类通过继承相关联。

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

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

第 1 步,创建一个array_view类。 它有一个beginend指针和方法以及一个size方法以及您可能期望的所有内容。

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

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

第四步,编写一个转换迭代器。 这是一种类型,其存储在一个迭代value_type_1其中或者调用一个函数,或隐式转换为常量性超过value_type_2

第 5 步,编写一个range_view< implicit_converting_iterator< T*, U* > >返回函数,用于何时T*可以隐式转换为U*

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

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 完成的。

这不起作用,因为

  1. 您没有返回指针或引用,这是协变返回工作所必需的;
  2. Foo<B>Foo<B>没有继承关系,无论FooAB (除非有特殊化,否则)。

但我们可以解决这个问题。 首先,请注意std::vector<A*>std::vector<B*>不能相互替代,无论任何语言限制如何,仅仅因为std::vector<B*>不支持添加A*元素。 因此,您甚至无法编写使std::vector<B*>替代std::vector<A*>的自定义适配器

但是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<A*>仍然与readonly_vector_view<B*>无关! 继续阅读...

事实证明,协变返回类型并不是真正必要的,它们是 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<A*> ,和B::test()返回一个readonly_vector_view<B*> 由于这些函数现在不是虚拟的,因此不需要它们的返回类型有任何关系。 一个只是隐藏另一个。 但是在内部,他们调用了一个虚函数,该函数创建(比如说)一个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));
    }
};

小菜一碟! 现场演示完全值得付出努力!

暂无
暂无

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

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