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.

Конечно, я могу легко создать соответствующую функцию, но она длиннее однострочного 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);
}

person Rafal    schedule 23.07.2016    source источник
comment
Является ли ваш код реальным вариантом использования или это просто пример? Если это реальный вариант использования, ИМХО, это плохое использование reinterpret_cast. Если это пример, укажите реальный вариант использования того, чего вы хотите достичь.   -  person rgmt    schedule 23.07.2016
comment
@wasthishelpful более или менее реальный вариант использования; подача поддельного объекта базы данных во время модульных тестов   -  person Rafal    schedule 23.07.2016
comment
В вашем примере вы могли бы просто использовать вектор Base в своей основной функции или использовать шаблоны в своей функции обновления. Я думаю, это невозможно в реальном случае, но из кода, который вы разместили, я не понимаю, почему :)   -  person rgmt    schedule 23.07.2016


Ответы (2)


reinterpret_casting, как это, является неопределенным поведением. Код сломается, если приведение от 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));
person Leon    schedule 23.07.2016
comment
Я знаю о поломке, когда требуется настройка указателя. Хотя в данной конкретной ситуации это не так. Хорошая идея с container_cast, я полностью пропустил конструкторы диапазонов. - person Rafal; 23.07.2016

Шаблоны вообще и контейнеры (я рассматриваю 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
person Jens    schedule 23.07.2016
comment
Хороший момент со строгим псевдонимом, хотя clang-3.8 принимает код. - person Rafal; 23.07.2016
comment
@Rafal Это может быть, но это все еще неопределенное поведение. Gcc также принимает его, если вы не компилируете с -fstrict-aliasing. - person Jens; 23.07.2016
comment
@Rafal Принятие кода также нормально, если его поведение не определено. Он может просто делать что угодно. - person Jens; 24.07.2016