Почему static_cast нельзя использовать для понижения, когда задействовано виртуальное наследование? - PullRequest
38 голосов
/ 20 сентября 2011

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

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 из наследования делает код действительным.

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

Ответы [ 6 ]

35 голосов
/ 20 сентября 2011

Техническая проблема заключается в том, что из 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.

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

4 голосов
/ 10 июня 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 использует, а требует его существования! Таким образом, последнее приведение может использоваться только для тех классов, которые содержат хотя бы одну виртуальную функцию (например, виртуальный деструктор)

2 голосов
/ 20 сентября 2011

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

2 голосов
/ 20 сентября 2011

Рассмотрим следующую функцию 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
1 голос
/ 20 сентября 2011

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

1 голос
/ 20 сентября 2011

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

virtual ism - явление во время выполнения.

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

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

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

...