2

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

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*>应该是协变的,但事实似乎并非如此。为什么?

4

7 回答 7

4

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

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

例如,std::vector<B*>不支持push_back(A*)运算,所以不可替代。

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

于 2014-09-12T17:17:37.390 回答
2

苹果是一种水果。

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

于 2014-09-12T17:17:51.567 回答
2

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

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++.
}
于 2014-09-12T17:21:08.780 回答
2

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

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

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

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

您的功能在第一点失败,即使您绕过它,第二点也会失败 -std::vector<A*>不是std::vector<B*>.

于 2014-09-12T17:22:33.203 回答
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返回一个向量,其中一些实际上是车辆,你会有什么感觉?它似乎打败了类型系统的观点,对吧?

于 2014-09-12T17:17:03.947 回答
0

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

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

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

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

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

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

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

第 5 步,编写一个range_view< implicit_converting_iterator< T*, U* > >返回函数,用于何时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 完成的。

于 2014-09-12T17:55:06.090 回答
0

这不起作用,因为

  1. 您没有返回指针或引用,这是协变返回工作所必需的;和
  2. Foo<B>并且Foo<B>不管Foo,A和都没有继承关系B(除非有专门化使它如此)。

但我们可以解决这个问题。首先,请注意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现在为及其迭代器创建一个具体的包装器,以便它包含一个指向 a 的指针,并将其操作委托给 a 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*and 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()返回 areadonly_vector_view<A*>B::test()返回 a 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));
    }
};

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

于 2014-09-12T19:07:23.660 回答