reinterpret_cast вектор указателей на вектор указателей на базовый класс

Рассмотрим следующий фрагмент кода

#include <algorithm>
#include <iostream>
#include <memory>
#include <vector>

struct Base {
    int x;

    Base(int x) : x(x) {}
};

struct Derived : public Base {
    int y, z;

    Derived(int x) : Base(x), y(x + 1), z(x + 2) {}
};

void update(const std::vector<std::shared_ptr<const Base>>& elements) {
    for (const auto elem : elements) {
        std::cout << elem->x << "\n";
    }
}

int main(int, char**) {
    std::vector<std::shared_ptr<Derived>> elements(4);

    {
        int ctr = 0;
        std::generate(begin(elements), end(elements), [&ctr]() { return std::make_shared<Derived>(++ctr); });
    }

//    update(elements); // note: candidate function not viable: no known conversion from 'vector<shared_ptr<Derived>>' to 'const vector<shared_ptr<const Base>>' for 1st argument
    update(reinterpret_cast<std::vector<std::shared_ptr<const Base>>&>(elements));  // ok

    return 0;
}

Мой вопрос, если с помощью reinterpret_cast изгонять из std::vector<std::shared_ptr<Derived>> в std::vector<std::shared_ptr<const Base>>& выполнимо и принято стандартом.

Я скомпилировал код с Clang-3.8 и GCC-6.1 с -fsanitize=undefined и похоже, что все в порядке. Тем не менее, я, кажется, не могу найти правильное объяснение по cppreference.

Конечно, я легко могу создать функцию appriopriate, но она длиннее однострочного reinterpret_cast и требует временного вектора.

void update(const std::vector<std::shared_ptr<Derived>>& elements) {
    std::vector<std::shared_ptr<const Base>> casted(elements.size());
    std::copy(begin(elements), end(elements), begin(casted));
    update(casted);
}

2 ответа

Шаблоны вообще и контейнеры (лечу shared_ptr как особая форма контейнера) не являются ковариантными в C++. Это означает, что если у вас есть два типа Base а также Derived < Base и template<typename T> class X {};, X<Base> а также X<Derived> это две совершенно разные вещи и не в какой-либо форме отношений.

В вашем случае у вас есть объект типа std::vector<std::shared_ptr<Derived>> а затем создать std::vector<std::shared_ptr<const Base>>& который затем используется для доступа к нему. Я думаю, что это имеет две проблемы:

  1. Вы бросаете объект типа vector в ссылочный тип. Мне действительно интересно, почему это работает.
  2. Вы получаете доступ к объекту через ссылку другого типа. Я думаю, что это нарушает строгое правило псевдонимов и, таким образом, является неопределенным поведением.

Если вы компилируете свой код с gcc -fstrict-aliasingкомпилятор предположит, что ваша программа соответствует правилу, и оптимизирует его. Будет сгенерировано предупреждение:

> Start prog.cc: In function 'int main(int, char**)': prog.cc:33:80:
> warning: dereferencing type-punned pointer will break strict-aliasing
> rules [-Wstrict-aliasing]
>      update(reinterpret_cast<std::vector<std::shared_ptr<const Base>>&>(elements));  // ok

reinterpret_castкак будто это неопределенное поведение. Код сломается при приведении из Derived* в Base* требует настройки указателя. Это, скорее всего, произойдет, когда Derived использует множественное наследование и Base это не первый базовый класс.

struct Derived : public X, public Base { ... };

Derived* d = new Derived;

Base* b = d; // this is no longer a "no-op", since the Base sub-object
             // of Derived is not at offset 0:
             //
             // d  b
             // |  |
             // v  v
             // [Derived]
             // [X][Base]

Если ваша цель состоит в том, чтобы заставить его работать максимально кратко, а не избегать преобразования через временный вектор, то в этом конкретном случае вы можете использовать container_cast Утилита предложена в этом ответе.

update(container_cast(elements));
Другие вопросы по тегам