Почему обращение к указателю на еще не созданный производный класс допустимо, но не является неопределенным поведением. godbolt.org
#include <iostream>
struct A{
int a;
void foo() {
std::cout << "A = " << a << std::endl;
}
};
struct B : public A{
int b;
void foo() {
std::cout << "B = " << b << std::endl;
}
};
int main() {
A *a = new A();
B *b = static_cast<B*>(a);
a->foo(); // cout A = 0
b->foo(); // cout B = 0
b->b = 333;
b->foo(); // cout B = 333
a->foo(); // cout A = 0
}
Должен ли указатель на производный класс быть неопределенным?
Что такое «неопределенный указатель»?
Разыменование указателя на производный класс, который на самом деле не указывает на такой производный класс, является поведением undefined.
«Неопределенное поведение» не означает какое-либо конкретное поведение. Невозможно наблюдать отсутствие неопределенного поведения.
Ваша ссылка Godbold не имеет отношения к делу, потому что, как упоминалось выше, это UB, и любой компилятор может делать все, что захочет.
Попробуйте скомпилировать с -O3 -Wall -Werror, чтобы получить довольно информативное сообщение об ошибке godbolt.org/z/6WfKsTb8T Тогда учтите, что код имеет точно такую же проблему и без -O3.
Неопределенное поведение не означает, что ваш код гарантированно рухнет или сделает что-то странное. Худшее поведение UB — это когда кажется, что он работает, даже если он полностью сломан, насколько говорит язык.
На самом деле сообщение об ошибке довольно забавное :)
Новички, кажется, продолжают верить, что неопределенное поведение означает, что «программа должна рухнуть или сделать что-то ужасное». На самом деле это просто означает, что стандарт не формулирует никаких требований. Довольно часто случаи неопределенного поведения выглядят "правильно" и не делают ничего ужасного - и этот вопрос, похоже, основан на этом примере.
Static_cast указатель на указатель на производный класс, когда объект, на который указывает, на самом деле не является подобъектом базового класса объекта производного типа, как вы делаете с приведением в B *b = static_cast<B*>(a);, имеет неопределенное поведение.
Неопределенное поведение не означает, что вы гарантированно получите ошибку или непреднамеренное поведение. Однако неопределенное поведение означает, что у вас также не будет никаких гарантий для предполагаемого поведения.
Скомпилируйте с -O3 -Werror -Wall, чтобы получить следующее сообщение от gcc:
<source>: In function 'int main()':
<source>:25:8: error: array subscript 'B[0]' is partly outside array bounds of 'unsigned char [4]' [-Werror=array-bounds]
25 | b->b = 333;
| ~~~^
<source>:19:18: note: object of size 4 allocated by 'operator new'
19 | A *a = new A();
| ^
cc1plus: all warnings being treated as errors
Сообщение несколько забавное, потому что оно пропускает некоторые детали реализации gcc, упоминая unsigned char [4], когда его нет. Однако, если вы подумаете, что не так в вашем коде, сообщение прибивает это....
В памяти объект B выглядит так (очень упрощенно):
A
B
У него есть подобъект A, после которого идут B члены. Теперь получается, что A имеет размер 4 (может быть другим, но это то, что с gcc).
Когда пишешь b->b. Затем b указывает на A (не на ba B), но вы делаете вид, что он указывает на B. Следовательно, когда компилятор пытается применить смещение к указателю для достижения члена, это смещение больше, чем размер объекта. Компилятор понимает, что что-то не может быть правильным.
Однако это сообщение об ошибке не более чем интересное любопытство. Компилятору не требуется диагностировать вашу ошибку, потому что ваш код имеет неопределенное поведение.
Когда ваш код ведет себя неопределенно, может случиться что угодно. В худшем случае код выглядит нормально и работает так, как ожидалось.
Почему ваш код компилируется и работает нормально? Это не. b не указывает на B. Ваш код может также вывести "Hello World" на консоль, он может стереть ваш жесткий диск или действительно может произойти что угодно. Только если ваш код не имеет неопределенного поведения, есть гарантия того, что ваша программа будет делать.
Также стоит отметить, что, скорее всего (но не гарантировано), на самом деле происходит то, что b->b = 333 перезаписывает 4 байта памяти после объекта A. Что, пока вы отмечаете важность, кажется, работает - по крайней мере, пока вы не сделаете больше в программе, и эти 4 байта фактически не начнут содержать вещи, которые не должны быть перезаписаны, после чего вы можете начать гоняться за «волшебными» ошибками. из несвязанных переменных, которые, кажется, меняют значение, когда никто не записывает их.
@Frodyne да, хотя, по-видимому, компиляция обнаруживает проблему и, таким образом, может даже полностью удалить b->b = 333;. Это просто не оптимизация, от которой действующая программа много выигрывает, поэтому маловероятно, что компилятор сделает это даже с -O3
Как вы думаете, почему это не неопределенное поведение?