Защо 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. Dynamic cast като цяло не го прави. (Има известно припокриване; динамичното предаване може да се използва за възходящо предаване.) - person David Hammen; 21.09.2011
comment
@David Това съответства на това, което разбирам е било намерението на комисията. Въпреки че RTTI не съществуваше по това време. Грубо казано, за невиртуално извличане, генерираният код трябва да провери за нула, след което да добави или извади константа; с виртуално извличане, константата се заменя с запис в vtable, което изисква два достъпа до паметта, за да се получи. IMHO, те трябваше да го направят така или иначе; два достъпа до паметта не са краят на света. Но комисията смяташе друго. - person James Kanze; 21.09.2011

static_cast е конструкция по време на компилация. той проверява валидността на cast по време на компилиране и дава грешка при компилация, ако е невалидно cast.

virtualism е явление по време на изпълнение.

И двете не могат да вървят заедно.

C++03 Standard §5.2.9/2 и §5.2.9/9 са подходящи в този случай.

Rvalue от тип „указател към 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. В противен случай резултатът от преобразуването е недефиниран .

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: Мисля, че е така, защото стандартът изрично забранява Base клас да бъде виртуален. Просто търся правилния цитат. Томалак ме изпревари, също придирчивост вашият пример няма виртуален. - 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