Недефинирано поведение ли е прехвърлянето от базов клас към производен?

Сблъсквал съм се с проблем, при който кастингът към производния клас би решил проблема. Намерих отговор на S.O, който казва, че може да доведе до UB, като го тествах, той се компилира и работи правилно. Това недефинирано поведение ли е? Ако е, какъв би бил правилният подход към този проблем?

class A
{
public:
    A(){};
    ~A(){}
};

class B : public A
{
public:
    B(){};
    ~B(){}
    void Show() { std::cout << "Show" << std::endl; }
};

int _tmain(int argc, _TCHAR* argv[])
{
    A a;
    B* b = static_cast<B*>(&a);
    b->Show();

    return 0;
}

person snoopy    schedule 16.02.2013    source източник


Отговори (3)


Докато указателят към базов тип действително сочи към екземпляр на производен тип, тогава такова използване не е недефинирано според стандарта C++. Въпреки това във вашия примерен код указателят b не сочи към екземпляр на B или някой от неговите производни типове (които няма), той сочи към екземпляр от A. Така че вашият код всъщност извиква недефинирано поведение.

Намерих отговор на S.O, който казва, че може да доведе до UB, като го тествах, той се компилира и работи правилно.

Фактът, че някои кодове се компилират и работят правилно, не изключва възможността кодът да извиква недефинирано поведение, тъй като недефинираното поведение включва „изглежда, че работи“. Причината, поради която трябва да избягвате недефинирано поведение, е, че няма гаранция, че то ще работи по същия начин следващия път, когато извикате UB.

Това недефинирано поведение ли е? Ако е, какъв би бил правилният подход към този проблем?

Във вашата извадка, да, това е недефинирано поведение. Правилният подход ще зависи от това какво всъщност трябва да прави вашият код, тъй като примерът, който предоставяте, е в най-добрия случай академичен пример.

За да стане ясно, следната модификация на вашата функция main() има добре дефинирано поведение и е изрично разрешена от стандарта C++:

B objectB;
A* ptrA = &objectB;

B* b = static_cast<B*>(ptrA);
b->Show();

Тук той е добре дефиниран, защото указателят ptrA всъщност сочи към екземпляр на B, въпреки че самият указател има тип A*. В горния пример преобразуването от A* към B* и след това извикването на една от функциите на B върху преобразувания указател ще работи. Разликата е, че в примера във вашия въпрос b всъщност не сочи към екземпляр на B.


Съответната клауза (акцентът е мой):

C++ стандарт 5.2.9/8 Статично предаване [expr.static.cast]

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 In silico    schedule 16.02.2013
comment
За подробно обяснение на UB вижте en.wikipedia.org/wiki/Undefined_behavior - person vonbrand; 17.02.2013

Прехвърлянето е разрешено (и следователно работи), ако посоченият обект наистина е B. Във вашия пример това е обикновен A и следователно получавате недефинирано поведение.

Което остави въпроса защо работи? Работи (или изглежда работи), защото методът, който сте извикали, няма достъп до обекта. По-вероятно е да се провали, ако добавите някои членски променливи към B и се опитате да получите достъп до тях в Show() или ако направите Show виртуална функция. Но във всеки случай това е UB, така че общо взето всичко може да се случи.

person Daniel Frey    schedule 16.02.2013
comment
В този прост случай няма никакъв проблем, свързан с достъпа до членове в базовия клас. Членовете ще бъдат на „правилното място“, така да се каже. &a.some_member == &(b->some_member) и следователно методът Show() трябва да работи дори ако е бил модифициран да отпечатва стойностите на всички членове. (Сега, разбира се, може да се говори за UB, за да се отхвърли целият този въпрос! Но ако ще се опитаме да обясним/предскажем какво може да се случи, мисля, че има ситуации, в които е трудно да се види на практика как един компилатор може да реши да направи „грешното“ нещо.) - person Aaron McDaid; 02.11.2013
comment
@AaronMcDaid Дори ако приемете, че има обекти, съвместими с оформлението, пак ще изглежда, че работи. Ако кажете, че трябва да работи, това е грешно, тъй като все още е незаконно от гледна точка на стандарта. Това е недефинирано поведение и компилаторът има право да прави всичко. И ако искате да прочетете заглавието на този въпрос: Става въпрос изрично за недефинирано поведение. - person Daniel Frey; 02.11.2013
comment
Здравей, @Daniel Frey, (редактирах коментара си леко, преди да видя отговора ти.) Надявам се, че коментарът ти сега не изглежда твърде неуместен. - person Aaron McDaid; 02.11.2013
comment
@AaronMcDaid Актуализирах и моя. - person Daniel Frey; 02.11.2013
comment
Освен това тези дискусии за UB ми напомнят за тази дискусия за отговори, в която участвах. Някои от нас се съгласиха, че UB не е т е „положително“ нещо. С други думи, ако други части на езика предоставят дефиниция на поведение, тогава UB в крайна сметка може да се дефинира. С други думи, когато стандартът казва, че X е недефинирано поведение, това наистина означава, че в този раздел няма какво да кажем за X. То може или не може да бъде дефинирано другаде, но този раздел няма коментари. - person Aaron McDaid; 02.11.2013
comment
Мисля, че мога да разделя това на по-полезен въпрос, който обмислям в момента в проект, върху който работя. Самото static_cast UB ли е веднага в точката на кастинг? Или може би е по-фин. Може би това просто означава, че стойността в каста, съхранена с B *b, е недефинирана? И следователно, че всъщност не преминаваме в UB, докато не се опитаме да дереферираме указателя? - person Aaron McDaid; 02.11.2013
comment
Зададох въпроса си по-подробно като отделен въпрос - person Aaron McDaid; 02.11.2013

Можете да прехвърлите указател към базов обект, който наистина сочи към производен екземпляр, към указател към извлечен обект.

Във вашия код обаче обектът, посочен от &a, не е производен обект, а основен обект и това, което правите, наистина е недефинирано поведение.

При имплементациите знам, че трябва да "работи", ако класът няма виртуални функции или бази и извлеченият обект не добавя никакви данни, а само методи. Все пак това е нещо, което официално не е гарантирано да работи.

Просто не го прави.

person 6502    schedule 16.02.2013