Подтвердить что ты не робот

Ковариация контейнеров в С++

Я знаю, что С++ не поддерживает ковариацию для элементов контейнера, как в Java или С#. Таким образом, следующий код, вероятно, имеет поведение undefined:

#include <vector>
struct A {};
struct B : A {};
std::vector<B*> test;
std::vector<A*>* foo = reinterpret_cast<std::vector<A*>*>(&test);

Неудивительно, что я получил downvotes, когда предлагаю это решение для другого вопроса.

Но какая часть стандарта С++ точно подсказывает мне, что это приведет к поведению undefined? Он гарантировал, что оба std::vector<A*> и std::vector<B*> сохраняют свои указатели в ограниченном блоке памяти. Это также гарантировало, что sizeof(A*) == sizeof(B*). Наконец, A* a = new B является совершенно законным.

Итак, какие плохие духи в стандарте я вызываю (кроме стиля)?

4b9b3361

Ответ 1

Нарушенное здесь правило документировано в С++ 03 3.10/15 [basic.lval], в котором указано, что неформально упоминается как "правило строгого сглаживания"

Если программа пытается получить доступ к сохраненному значению объекта через значение l, отличного от одного из следующих типов, поведение undefined:

  • динамический тип объекта,

  • cv-квалифицированная версия динамического типа объекта,

  • тип, который является подписанным или неподписанным типом, соответствующим динамическому типу объекта,

  • тип, который является подписанным или неподписанным типом, соответствующим квитанционной версии динамического типа объекта,

  • совокупность или тип объединения, который включает один из вышеупомянутых типов среди его членов (включая рекурсивно, член субагрегата или содержащегося объединения),

  • тип, который является (возможно, cv-квалифицированным) типом базового класса динамического типа объекта,

  • a char или неподписанный char тип.

Короче говоря, при заданном объекте вы можете получить доступ к этому объекту только через выражение, имеющее один из типов в списке. Для объекта класса, который не имеет базовых классов, например std::vector<T>, в основном вы ограничены типами, указанными в первой, второй и последней палитрах.

std::vector<Base*> и std::vector<Derived*> являются полностью несвязанными типами, и вы не можете использовать объект типа std::vector<Base*>, как если бы это был std::vector<Derived*>. Компилятор может делать всевозможные вещи, если вы нарушаете это правило, в том числе:

  • выполнять разные оптимизации на одном, а не на другом, или

  • выложите внутренние элементы одного по-другому или

  • выполнить оптимизацию, предполагая, что a std::vector<Base*>* никогда не может ссылаться на тот же объект, что и std::vector<Derived*>*

  • используйте проверки времени выполнения, чтобы убедиться, что вы не нарушаете правило строгого псевдонима

[Он также может не выполнять ничего из этого, и он может "работать", но нет гарантии, что он "сработает", и если вы измените компиляторы или версии компилятора или параметры компиляции, все это может остановить "работу". Здесь я использую цитаты с испугом.: -)]

Даже если у вас был только Base*[N], вы не могли использовать этот массив, как если бы это был Derived*[N] (хотя в этом случае использование, вероятно, было бы более безопасным, где "безопаснее" означает "все еще undefined, но менее вероятно, чтобы вы попали в беду).

Ответ 2

Вы вызываете плохой дух reinterpret_cast < > .

Если вы действительно не знаете, что делаете (я имею в виду не гордо, а не педантично), reinterpret_cast является одним из врат зла.

Единственное безопасное использование, о котором я знаю, - это управление классами и структурами между вызовами функций С++ и C. Возможно, некоторые другие.

Ответ 3

Общая проблема с ковариацией в контейнерах следующая:

Предположим, что ваш бросок будет работать и быть законным (это не значит, что допустим это для следующего примера):

#include <vector>
struct A {};
struct B : A { public: int Method(int x, int z); };
struct C : A { public: bool Method(char y); };
std::vector<B*> test;
std::vector<A*>* foo = reinterpret_cast<std::vector<A*>*>(&test);
foo->push_back(new C);
test[0]->Method(7, 99); // What should happen here???

Итак, вы также переинтерпретируете C1 на B *...

На самом деле я не знаю, как это делают .NET и Java (я думаю, что они генерируют исключение при попытке вставить C).

Ответ 4

Думаю, будет легче показать, чем сказать:

struct A { int a; };

struct Stranger { int a; };

struct B: Stranger, A {};

int main(int argc, char* argv[])
{
  B someObject;
  B* b = &someObject;

  A* correct = b;
  A* incorrect = reinterpret_cast<A*>(b);

  assert(correct != incorrect); // troubling, isn't it ?

  return 0;
}

Здесь (конкретная) проблема заключается в том, что при выполнении "правильного" преобразования компилятор добавляет некоторый указатель ajdustement в зависимости от расположения памяти объектов. На a reinterpret_cast настройка не выполняется.

Я полагаю, вы поймете, почему использование reinterpet_cast обычно должно быть запрещено из кода...