簡體   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