Что такое ковариантные типы возвращаемых данных в 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*> также должен быть ковариантным, но, похоже, это не так. Зачем?

1

Решение

Ковариантные возвращаемые типы позволяют переопределенным виртуальным функциям-членам в производном классе возвращать объект другого типа, при условии, что он может использоваться так же, как и тип возвращаемого базового класса. Компьютерные ученые (со времен Барбары Лисков) имеют теоретическое определение «может использоваться одинаково»: взаимозаменяемость.

Нет, std::vector<B*> не является подтипом std::vector<A*>и не должно быть.

Например, std::vector<B*> не поддерживает push_back(A*) операция, поэтому она не является заменяемой.

C ++ вообще не пытается выводить отношения подтипов для шаблонов. Отношения будут существовать только в том случае, если вы на самом деле специализируете их и определите базовый класс. Одна из причин этого, даже на интерфейсах, которые теоретически ковариантны (в основном, только для чтения), заключается в том, что версия C ++ на самом деле сильнее, чем подстановка Лискова — в C ++ совместимость должна существовать на двоичном уровне. Поскольку расположение в памяти коллекций связанных объектов может не соответствовать расположению подобъектов, эта двоичная совместимость не достигается. Ограничение ковариантных возвращаемых типов только указателями или ссылками также является следствием проблемы двоичной совместимости. Производный объект, вероятно, не поместится в пространство, зарезервированное для базового экземпляра … но его указатель будет.

4

Другие решения

Шаблоны не «наследуют» ковариацию, потому что разные специализации шаблонов могут быть на 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 вернул вектор, где некоторые из них на самом деле были транспортными средствами? Похоже, победить точку системы типов не так ли?

1

Яблоко — это фрукт.

Мешок с яблоками — это не мешок с фруктами. Это потому, что вы можете положить грушу в пакет с фруктами.

1

Стандарт определяет ковариацию для целей C ++ в §10.3 [class.virtual] / p7:

Тип возврата переопределяющей функции должен быть идентичным
тип возврата переопределенной функции или ковариант с
классы функций. Если функция D::f переопределяет функцию
B::fвозвращаемые типы функций ковариантны, если они
удовлетворяют следующим критериям:

  • оба являются указателями на классы, оба являются lvalue ссылками на классы, или оба являются rvalue ссылками на классы113
  • класс в возвращаемом типе B::f тот же класс, что и класс в типе возврата D::fили является однозначным и
    доступный прямой или косвенный базовый класс класса в возвращении
    тип D::f
  • оба указателя или ссылки имеют одинаковую квалификацию cv и тип класса в возвращаемом типе D::f имеет ту же квалификацию
    как или меньше cv-квалификации, чем тип класса в возвращаемом типе
    B::f,

113Многоуровневые указатели на классы или ссылки на многоуровневые указатели на классы не допускаются.

Ваши функции терпят неудачу в первой точке, и, даже если вы обойдете ее, во второй — std::vector<A*> не является основой std::vector<B*>,

1

C ++ FAQ отвечает на это прямо в [21.3] Является ли автостоянка автомобиля своего рода автостоянкой? («Тебе не обязательно это нравиться. Но ты должен принять это».)

ТАК вопрос Получение вектора в функцию, которая ожидает вектор спрашивает то же самое. И ответ заключается в том, что, хотя на первый взгляд кажется безопасным разрешать ковариацию универсальных типов, в частности контейнеров производного типа, рассматриваемых как контейнеры базового типа, это довольно небезопасно.

Рассмотрим этот код:

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

Ковариация происходит только тогда, когда вы возвращаете указатель или ссылку на класс, а классы связаны наследованием.

Это явно не происходит, потому что std::vector<?> не указатель, ни ссылка, а потому что два std::vector<?>у них нет родителя / ребенка.

Теперь мы можем сделать эту работу.

Шаг 1, создайте array_view учебный класс. Оно имеет begin а также end указатель и методы и size метод и все, что вы могли ожидать.

Шаг 2, создайте shared_array_view, который является представлением массива, который также владеет shared_ptr<void> с пользовательским удалителем: в остальном он идентичен. Этот класс также гарантирует, что просматриваемые данные сохраняются достаточно долго для просмотра.

Шаг 3, создайте range_view, которая является парой итераторов и одевается на нее. Сделайте то же самое с shared_range_view с токеном собственности. Изменить ваш array_view быть range_view с некоторыми дополнительными гарантиями (в основном смежные итераторы).

Шаг 4, напишите итератор преобразования. Это тип, который хранит итератор value_type_1 который либо вызывает функцию, либо неявно преобразуется в const_iterator 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_());
}
};

Большая часть того, что я описываю, была сделана бустом.

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 и его итератор, так что он содержит указатель и делегирует свои операции 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; }

(* Он) -> оператор -> (); }
трубчатый проход& operator ++ () {++ * it; вернуть * это; }
трубчатый проход& оператор — () {- * это; вернуть * это; }
оператор iter ++ (int) {iter n (* this); ++ * его; вернуть n; }
оператор iter — (int) {iter n (* this); —*Это; вернуть n; }
оператор bool == (постоянный& другое) const {return * it == * other.it; }
оператор bool! = (постоянный& другое) const {return * it! = * other.it; }
частный:
std :: unique_ptr 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 который смотрит на вектор разнотипных элементов:

шаблон
struct readonly_vector_view_impl: readonly_vector_view_base
{
typedef имя типа readonly_vector_view_base :: 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; }

it.operator -> (); }
трубчатый проход& operator ++ () {++ it; вернуть * это; }
трубчатый проход& оператор — () {++ it; вернуть * это; }

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

Кусок пирога! Живая демо Абсолютно стоит усилий!

0