Беги
Удаление производного объекта через указатель на базу при отсутствии виртуального деструктора является неопределенным поведением. Это верно независимо от того, насколько прост производный тип.
Теперь во время выполнения каждый компилятор превращает delete foo
в «найти код деструктора, запустить его, затем очистить память». Но вы не можете основывать свое понимание того, что означает код C ++ на основе кода времени выполнения, испускаемого компилятором.
Так что вы наивно можете подумать: «Мне все равно, если мы запустим неправильный код уничтожения; единственное, что я добавил, это int
. А код очистки памяти обрабатывает перераспределение. Так что мы хороши!»
Вы даже идете и тестируете его, смотрите на произведенную сборку, и все работает! И вы заключаете, что здесь нет проблем.
Вы не правы.
Компиляторы делают две вещи. Во-первых, код выполнения emit. Во-вторых, они используют структуру вашей программы, чтобы рассуждать об этом.
Эта вторая часть является мощной функцией, но она также делает выполнение неопределенного поведения чрезвычайно опасным.
Что означает ваша программа C ++ в «абстрактной машине», стандарт C ++ определяет , значение . Именно в этой абстрактной машине происходят оптимизации и преобразования кода. Знание того, как на вашем физическом компьютере генерируется изолированный фрагмент кода, не говорит вам, что делает этот фрагмент кода.
Вот конкретный пример:
struct Foo {};
struct Bar:Foo{};
Foo* do_something( bool cond1, bool cond2 ) {
Foo* foo = nullptr;
if (cond1)
foo = new Bar;
else
foo = new Foo;
if (cond2 && !cond1)
inline_code_to_delete_user_folder();
if (cond2) {
delete foo;
foo = nullptr;
}
return foo;
}
вот игрушка с некоторыми типами игрушек.
В нем мы создаем указатель на Bar
или Foo
на основе cond1
.
Тогда мы можем сделать что-то опасное.
Наконец, если cond2
истинно, мы очищаем Foo* foo
.
Дело в том, что если мы называем delete foo
и foo
не является Foo
, это неопределенное поведение. Компилятор может обоснованно обосновать: «Хорошо, мы вызываем delete foo
, поэтому *foo
является объектом типа Foo
».
Но если foo
является указателем на фактический Foo
, то, очевидно, cond1
должно быть ложным , потому что только когда оно ложно, foo
указывает на фактическое Foo
.
Таким образом, логически, cond2
истинно, подразумевает cond1
верно. Всегда. Везде. Заднее число.
Таким образом, компилятор действительно знает, что это законное преобразование вашей программы:
Foo* do_something( bool cond1, bool cond2 ) {
if (cond2) {
Foo* foo = new Foo;
inline_code_to_delete_user_folder();
delete foo;
return nullptr;
}
Foo* foo = nullptr;
if (cond1)
foo = new Bar;
else
foo = new Foo;
return foo;
}
что довольно опасно, не правда ли? Мы просто отказались от проверки cond1
и удаляли пользовательскую папку всякий раз, когда вы передавали true
в cond2
.
Я не знаю, используют ли какие-либо текущие или будущие компиляторы обнаружение UB при удалении неправильного типа для логического обратного распространения ветвей UB, но компиляторы делают нечто подобное с другими видами UB, даже такие безобидные вещи, как целочисленное переполнение со знаком.
И чтобы этого не происходило, вам нужно проверять каждую оптимизацию каждого компилятора каждого компилятора, который когда-либо будет компилировать ваш код.
Беги