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

Почему static_cast не может использоваться для down-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 из наследования делает код действительным.

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

4b9b3361

Ответ 1

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

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

Ответ 2

Рассмотрим следующую функцию 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

Ответ 3

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

Ответ 4

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

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

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

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

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

Ответ 5

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

Ответ 6

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