Является ли неопределенным поведением переход от базового класса к производному?

Я столкнулся с проблемой, когда приведение к производному классу решило бы проблему. Я нашел ответ на SO, в котором говорится, что это может привести к 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. Таким образом, ваш код действительно вызывает неопределенное поведение.

Я нашел ответ на SO, в котором говорится, что это может привести к 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.


Соответствующий пункт (выделено мной):

Стандарт С++ 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 виртуальной функцией. Но в любом случае это УБ, так что в принципе всякое бывает.

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