Почему нельзя использовать static_cast для приведения вниз, когда задействовано виртуальное наследование?

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

struct Base {};
struct Derived : public virtual Base {};

void f()
{
    Base* b = new Derived;
    Derived* d = static_cast<Derived*>(b);
}

Это запрещено стандартом ([n3290: 5.2.9/2]), поэтому код не компилируется, поскольку Derived фактически наследуется от Base. Удаление virtual из наследства делает код действительным.

Какова техническая причина существования этого правила?


person eran    schedule 20.09.2011    source источник
comment
Я надеюсь, что вы довольны моей правкой.   -  person Lightness Races in Orbit    schedule 20.09.2011


Ответы (6)


Техническая проблема заключается в том, что из Base* невозможно определить смещение между началом подобъекта Base и началом объекта Derived.

В вашем примере это выглядит нормально, потому что в поле зрения есть только один класс с базой Base, и поэтому кажется неуместным, что наследование является виртуальным. Но компилятор не знает, определил ли кто-то другой class Derived2 : public virtual Base, public Derived {}, и приводит Base*, указывающий на подобъект Base этого. В общем[*] смещение между подобъектом Base и подобъектом Derived в пределах Derived2 может не совпадать со смещением между подобъектом Base и полным объектом Derived объекта, чей наиболее производный тип — Derived, именно потому, что Base является практически унаследовано.

Таким образом, нет никакого способа узнать динамический тип всего объекта и различные смещения между указателем, который вы дали приведению, и требуемым результатом, в зависимости от того, что это за динамический тип. Следовательно, бросок невозможен.

Ваш Base не имеет виртуальных функций и, следовательно, не имеет RTTI, поэтому, безусловно, нет способа определить тип полного объекта. Актерский состав по-прежнему забанен, даже если у Base есть RTTI (сразу не знаю, почему), но я думаю, не проверяя, что в этом случае возможен dynamic_cast.

[*] что я имею в виду, если этот пример не доказывает точку зрения, продолжайте добавлять больше виртуального наследования, пока не найдете случай, когда смещения отличаются ;-)

person Steve Jessop    schedule 20.09.2011
comment
+1: у меня заболела голова, поэтому я не скажу, что это совершенно четкий ответ, но мне кажется, что он правильный. :) - person Lightness Races in Orbit; 20.09.2011
comment
@Tomalak: спасибо, у меня тоже немного болит голова, но если я придумаю способ объяснить более четко, я вернусь и отредактирую. - person Steve Jessop; 20.09.2011
comment
*кашель* Диаграммы ASCII! *кашель* - person Lightness Races in Orbit; 20.09.2011
comment
@Tomalak: да, требуется больше времени, чем у меня есть, и я все равно добираюсь до магазинов во время обеденного перерыва. Мне пришлось бы написать несколько классов с виртуальным наследованием и выяснить их макеты, чтобы зафиксировать несоответствие, вместо того, чтобы махать рукой в ​​этой части... - person Steve Jessop; 20.09.2011
comment
Конечно. Это чуть больше, чем я тоже готов сделать, хотя я все равно хотел бы это увидеть! - person Lightness Races in Orbit; 20.09.2011
comment
@Steve Единственный известный мне способ ясно объяснить реальную проблему - это нарисовать (типичную) схему в случае Derived и Derived2. И делать такие диаграммы в ASCII очень больно. - person James Kanze; 20.09.2011
comment
Возможен ли вообще dynamic_cast? - person Aaron McDaid; 28.07.2015
comment
Это объяснение недостаточно ясно для понимания. Не могли бы вы рассказать об этом подробнее? - person Hemant Bhargava; 09.03.2017

static_cast может выполнять только те приведения, где расположение памяти между классами известно во время компиляции. dynamic_cast может проверять информацию во время выполнения, что позволяет более точно проверять правильность приведения, а также считывать во время выполнения информацию о структуре памяти.

Виртуальное наследование помещает в каждый объект информацию о времени выполнения, которая определяет расположение памяти между Base и Derived. Один следует за другим или есть дополнительный промежуток? Поскольку static_cast не может получить доступ к такой информации, компилятор будет действовать консервативно и просто выдаст ошибку компиляции.


Подробнее:

Рассмотрим сложную структуру наследования, где из-за множественного наследования существует несколько копий Base. Наиболее типичный сценарий — бриллиантовое наследство:

class Base {...};
class Left : public Base {...};
class Right : public Base {...};
class Bottom : public Left, public Right {...};

В этом сценарии Bottom состоит из Left и Right, где каждый имеет собственную копию Base. Структура памяти всех вышеперечисленных классов известна во время компиляции, и static_cast можно использовать без проблем.

Давайте теперь рассмотрим аналогичную структуру, но с виртуальным наследованием Base:

class Base {...};
class Left : public virtual Base {...};
class Right : public virtual Base {...};
class Bottom : public Left, public Right {...};

Использование виртуального наследования гарантирует, что при создании Bottom он будет содержать только одну копию Base, которая совместно используется между частями объекта Left и Right. Макет объекта Bottom может быть, например:

Base part
Left part
Right part
Bottom part

Теперь предположим, что вы привели Bottom к Right (это правильное приведение). Вы получаете указатель Right на объект, который состоит из двух частей: Base и Right между ними есть пробел в памяти, содержащий (теперь неактуальную) часть Left. Информация об этом промежутке сохраняется во время выполнения в скрытом поле Right (обычно обозначается как vbase_offset). Подробности можно прочитать, например, здесь.

Однако разрыва не было бы, если бы вы просто создали автономный объект Right.

Итак, если я дам вам только указатель на Right, вы не будете знать во время компиляции, является ли он автономным объектом или частью чего-то большего (например, Bottom). Вам нужно проверить информацию во время выполнения, чтобы правильно преобразовать Right в Base. Вот почему static_cast не получится, а dynamic_cast нет.


Примечание о dynamic_cast:

Хотя static_cast не использует информацию об объекте во время выполнения, dynamic_cast использует и требует его существования! Таким образом, последнее приведение может использоваться только в тех классах, которые содержат хотя бы одну виртуальную функцию (например, виртуальный деструктор).

person CygnusX1    schedule 09.06.2017

Рассмотрим следующую функцию foo:

#include <iostream>

struct A
{
    int Ax;
};

struct B : virtual A
{
    int Bx;
};

struct C : B, virtual A
{
    int Cx;
};


void foo( const B& b )
{
    const B* pb = &b;
    const A* pa = &b;

    std::cout << (void*)pb << ", " << (void*)pa << "\n";

    const char* ca = reinterpret_cast<const char*>(pa);
    const char* cb = reinterpret_cast<const char*>(pb);

    std::cout << "diff " << (cb-ca) << "\n";
}

int main(int argc, const char *argv[])
{
    C c;
    foo(c);

    B b;
    foo(b);
}

Хотя эта функция не совсем переносима, она показывает нам «смещение» A и B. Поскольку компилятор может быть довольно либеральным в размещении подобъекта A в случае наследования (также помните, что самый производный объект вызывает виртуальный базовый ctor!), фактическое размещение зависит от «реального» типа объекта. Но так как foo получает ссылку только на B, любой static_cast (который работает во время компиляции, самое большее применяя некоторое смещение) обречен на неудачу.

ideone.com (http://ideone.com/2qzQu) выводит для этого:

0xbfa64ab4, 0xbfa64ac0
diff -12
0xbfa64ac4, 0xbfa64acc
diff -8
person PlasmaHH    schedule 20.09.2011

По сути, нет никакой реальной причины, но намерение состоит в том, чтобы static_cast было очень дешево, включающим самое большее добавление или вычитание константы к указателю. И нет никакого способа реализовать желаемый бросок так дешево; в основном, поскольку относительные положения Derived и Base внутри объекта могут измениться, если есть дополнительное наследование, преобразование потребует много накладных расходов dynamic_cast; члены комитета, вероятно, думали, что это опровергает причины использования static_cast вместо dynamic_cast.

person James Kanze    schedule 20.09.2011
comment
По сути, есть все причины. - person Lightness Races in Orbit; 20.09.2011
comment
Не минусующий, но я согласен с Томалаком. static в static_cast означает преобразование времени компиляции для ненулевого указателя. - person David Hammen; 20.09.2011
comment
@David Hammen Это зависит от того, что вы подразумеваете под преобразованием во время компиляции. static_cast определенно может включать некоторый код, выполняемый во время выполнения, даже если задействованы только указатели. Но, как я уже сказал, код довольно ограничен: добавление или вычитание константы из указателя, но не поиск в vtable. (Не уверен, почему голосование против, так как мой ответ полностью правильный. Но голоса здесь в любом случае кажутся довольно случайными; я не думаю, что они что-то значат.) - person James Kanze; 20.09.2011
comment
@JamesKanze: я имел в виду то, что ты сказал. Будет код времени выполнения, но он будет ограничен по своей природе: проверка нулевого указателя, добавление фиксированного смещения, определенного во время компиляции. Статическое литье работает без RTTI. Динамического литья вообще нет. (Есть некоторое перекрытие; динамическое приведение может использоваться для преобразования вверх.) - person David Hammen; 21.09.2011
comment
@David Это соответствует тому, что, как я понимаю, было намерением комитета. Хотя RTTI в то время не существовало. Грубо говоря, для невиртуального вывода сгенерированный код должен проверять значение null, а затем добавлять или вычитать константу; при виртуальном выводе константа заменяется записью в vtable, для получения которой требуется два обращения к памяти. ИМХО, они должны были это сделать в любом случае; два обращения к памяти — это не конец света. Но комитет считал иначе. - person James Kanze; 21.09.2011

static_cast — это конструкция времени компиляции. он проверяет действительность приведения во время компиляции и выдает ошибку компиляции, если недопустимо приведение.

virtualism — это феномен времени выполнения.

Оба не могут идти вместе.

Стандарт C++03 §5.2.9/2 и §5.2.9/9 актуальны в данном случае.

Значение r типа «указатель на cv1 B», где B — тип класса, может быть преобразовано в значение r типа «указатель на cv2 D», где D — класс, производный (пункт 10) от B, если действующий стандарт преобразование из «указателя на D» в «указатель на B» существует (4.10), cv2 — это та же самая cv-квалификация или более высокая cv-квалификация, чем cv1, и B не является виртуальным базовым классом D< /сильный>. Значение нулевого указателя (4.10) преобразуется в значение нулевого указателя целевого типа. Если rvalue типа «указатель на cv1 B» указывает на B, который на самом деле является подобъектом объекта типа D, результирующий указатель указывает на охватывающий объект типа D. В противном случае результат приведения не определен. .

person Alok Save    schedule 20.09.2011
comment
На самом деле это не ответ, так как вы можете понизить его с помощью static_cast, например. struct B {}; struct D : B {}; int main() { B* x = new D; D* y = static_cast<D*>(x); }. - person Cat Plus Plus; 20.09.2011
comment
Структура памяти Derived известна во время компиляции, независимо от используемого виртуального наследования. Кроме того, полиморфизм в целом является явлением среды выполнения, однако вам по-прежнему разрешено использовать static_cast для выполнения действий, которые могут оказаться неопределенными во время выполнения. - person eran; 20.09.2011
comment
@catPlusPlus: Где там virtual? - person Lightness Races in Orbit; 20.09.2011
comment
@Cat: Я думаю, это потому, что стандарт явно запрещает базовому классу быть виртуальным. Я как раз искал правильную цитату, Томалак меня опередил, К тому же придирки к твоему примеру нет виртуального. - person Alok Save; 20.09.2011
comment
@Tomalak: Да, я понимаю :) На самом деле чтение стандарта заставляет меня немного закружиться в голове, я думаю, я не могу его переварить, но я все равно пытаюсь. - person Alok Save; 20.09.2011
comment
@Als: Я думаю, что начинаю становиться невосприимчивым к этому, что вызывает головокружение. - person Lightness Races in Orbit; 20.09.2011
comment
@eran: расположение памяти Derived известно во время компиляции - однако этого недостаточно (см. Мой ответ). Тот факт, что у вас есть static_cast для Derived, не означает, что наиболее производным типом рассматриваемого объекта является Derived, и, следовательно, он может не иметь макета, который имеет Derived. Вот что означает виртуальное наследование, что подобъект Base не является частью подобъекта Derived в классах, производных от Derived. - person Steve Jessop; 20.09.2011
comment
Предложенный ответ неверен. static_cast не проверяет достоверность; учитывая что-то вроде static_cast<Derived*>( pBase ), если pBase на самом деле не имеет типа Derived, поведение не определено. Но static_cast компилируется нормально. - person James Kanze; 20.09.2011
comment
@James: Нет, static_cast неправильное. - person Lightness Races in Orbit; 20.09.2011

Я полагаю, это связано с тем, что классы с виртуальным наследованием имеют разную структуру памяти. Родитель должен быть разделен между детьми, поэтому только один из них может быть выложен непрерывно. Это означает, что вы не гарантируете, что сможете отделить непрерывную область памяти, чтобы рассматривать ее как производный объект.

person RocketR    schedule 20.09.2011