Контейнерная ковариация в C ++ - PullRequest
18 голосов
/ 26 января 2011

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

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

Не удивительно, что я получил отрицательные отзывы, когда предложил решение другого вопроса .

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

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

Ответы [ 4 ]

16 голосов
/ 26 января 2011

Нарушенное здесь правило задокументировано в C ++ 03 3.10 / 15 [basic.lval], в котором указано, что неофициально называют «правилом строгого алиасинга»

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

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

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

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

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

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

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

  • тип char или unsigned char.

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

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

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

  • выложите внутренние элементы одного из них, или

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

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

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

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

4 голосов
/ 26 января 2011

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

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

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

3 голосов
/ 26 января 2011

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

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

#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???

Таким образом, вы также реинтерпретировали приведение C * к B * ...

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

2 голосов
/ 26 января 2011

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

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

Показанная здесь (специфическая) проблема заключается в том, что при выполнении «правильного» преобразования компилятор добавляет некоторые настройки указателя в зависимости от расположения объектов в памяти. На reinterpret_cast регулировка не выполняется.

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

...