Ответ 1
Программа содержит поведение undefined, как разыменование нулевого указателя
(т.е. вызов foo()
в главном, не присваивая ему действительный адрес
заранее) является UB, поэтому стандарты не налагаются.
Выполнение never_called
во время выполнения - идеальная действительная ситуация, когда
Повреждение undefined было удалено, оно так же корректно, как и просто сбой (например,
при компиляции с GCC). Хорошо, но почему Кланг это делает? если ты
скомпилируйте его с отключением оптимизации, программа больше не будет выводить
"форматирование жесткого диска" и просто сбой:
$ clang++ -std=c++17 -O0 a.cpp && ./a.out
Segmentation fault (core dumped)
Сгенерированный код для этой версии выглядит следующим образом:
main: # @main
push rbp
mov rbp, rsp
call qword ptr [foo]
xor eax, eax
pop rbp
ret
Он пытается сделать вызов функции, для которой foo
указывает, и как foo
инициализируется с помощью nullptr
(или если у него нет инициализации,
это все равно будет иметь место), его значение равно нулю. Здесь undefined
поведение было поражено, поэтому все может произойти вообще и программа
оказывается бесполезным. Как правило, обращение к такому недействительному адресу
приводит к ошибкам ошибок сегментации, поэтому сообщение мы получаем, когда
выполнение программы.
Теперь рассмотрим одну и ту же программу, но скомпилируем ее с оптимизациями:
$ clang++ -std=c++17 -O3 a.cpp && ./a.out
formatting hard disk drive!
Сгенерированный код для этой версии выглядит следующим образом:
set_foo(): # @set_foo()
ret
main: # @main
push rax
mov edi, .L.str
call puts
xor eax, eax
pop rcx
ret
.L.str:
.asciz "formatting hard disk drive!"
Интересно, что каким-то образом оптимизация изменила программу так, что
main
вызывает std::puts
напрямую. Но почему Кланг это сделал? И почему
set_foo
скомпилирован в одну инструкцию ret
?
Вернемся к стандарту (N4660, в частности) на мгновение. Какие это говорит о undefined поведении?
3.27 undefined поведение [defns.undefined]
для которого этот документ не предъявляет требований
[Примечание: поведение Undefined может, когда этот документ опускает любое явное определение поведения или , когда программа использует ошибочную конструкцию или ошибочные данные. Допустимые диапазоны поведения undefinedот полностью игнорируя ситуацию с непредсказуемыми результатами, чтобы во время перевода или выполнение программы документированным образом характерной для окружающей среды (с выпуском или без диагностическое сообщение), до прекращения перевода или исполнения (с выдача диагностического сообщения). Многие ошибочные программные конструкции не порождают поведение undefined; они должны быть диагностированы. Оценка постоянного выражения никогда не демонстрирует поведения явно указанный как undefined ([expr.const]). - конечная нота]
Акцент на мой.
Программа, которая демонстрирует поведение undefined, становится бесполезной, поскольку все
он сделал до сих пор и не будет иметь никакого смысла, если он содержит
ошибочные данные или конструкции. Помня об этом, помните, что
компиляторы могут полностью игнорировать случай, когда поведение undefined
, и это фактически используется как обнаруженные факты при оптимизации
программа. Например, конструкция типа x + 1 > x
(где x
- целое число со знаком) будет скомпилирована для
true, даже если значение x
неизвестно во время компиляции. Обоснование
заключается в том, что компилятор хочет оптимизировать действительные случаи, и единственный
способ для того, чтобы эта конструкция была действительной, - это если она не вызывает арифметику
переполнение (т.е. если x != std::numeric_limits<decltype(x)>::max()
). Эта
это новый научный факт в оптимизаторе. Исходя из этого, конструкция
всегда подтверждается.
Примечание: эта же оптимизация не может произойти для целых чисел без знака, потому что переполнение одного из них не является UB. То есть, он должен сохранять выражение таким, какой он есть, потому что он может иметь другую оценку, когда он переполняется. Оптимизация его для целых чисел без знака будет несовместимой со стандартом (спасибо aschepler.)
Это полезно, поскольку позволяет тонны оптимизаций для удара
в. Так
далеко, так хорошо, но что произойдет, если x
имеет максимальное значение во время выполнения?
Ну, это поведение undefined, поэтому это бессмыслица, чтобы попытаться рассуждать о
это, как может случиться, и стандарт не предъявляет никаких требований.
Теперь у нас достаточно информации, чтобы лучше изучить ваши неисправности программа. Мы уже знаем, что доступ к нулевому указателю равен undefined поведение, и то, что вызывает забавное поведение во время выполнения. Поэтому попробуйте понять, почему оптимизирован Clang (или технически LLVM) программа так, как она была.
static void (*foo)() = nullptr;
static void never_called()
{
std::puts("formatting hard disk drive!");
}
void set_foo()
{
foo = never_called;
}
int main()
{
foo();
}
Помните, что до main
можно вызвать set_foo
начинает выполнение. Например, когда верхний уровень объявляет переменную,
вы можете вызвать его при инициализации значения этой переменной:
void set_foo();
int x = (set_foo(), 42);
Если вы пишете этот фрагмент до main
, программа no
более длинный демонстрирует поведение undefined, а сообщение "форматирование сложно
дисковод! ", при этом оптимизация будет включена или выключена.
Итак, каков единственный способ использования этой программы? Там это set_foo
функция, которая присваивает адрес never_called
foo
, поэтому мы можем
найти что-то здесь. Обратите внимание, что foo
обозначается как static
, что означает
имеет внутреннюю связь и не может быть доступен извне этого перевода
Блок. Напротив, функция set_foo
имеет внешнюю связь и может
доступ извне. Если другая единица перевода содержит фрагмент
как и выше, тогда эта программа станет действительной.
Прохладный, но никто не вызывает set_foo
извне. Хотя это
является тот факт, что оптимизатор видит, что единственный способ для этой программы -
допустимо, если set_foo
вызывается до main
, в противном случае это
только поведение undefined. Это новый научный факт, и он предполагает set_foo
на самом деле называется. Основываясь на этих новых знаниях, другие оптимизации, которые
удар может использовать его.
Например, когда constant
складывание
он видит, что конструкция foo()
действительна только в том случае, если foo
может быть правильно инициализирована. Единственный путь для этого - если set_foo
вызывается вне этой единицы перевода, поэтому foo = never_called
.
"Исправление мертвого кода" и могут обнаружить что если foo == never_called
, то код внутри set_foo
не нужен,
поэтому он преобразуется в одну инструкцию ret
.
Оптимизация встроенного расширения
видит, что foo == never_called
, поэтому вызов foo
можно заменить
с его телом. В итоге мы получим что-то вроде этого:
set_foo():
ret
main:
mov edi, .L.str
call puts
xor eax, eax
ret
.L.str:
.asciz "formatting hard disk drive!"
Это несколько эквивалентно выпуску Clang с оптимизациями. Конечно, то, что действительно сделал Кланг, может (и может) быть другим, но оптимизация, тем не менее, может прийти к такому же выводу.
Изучая вывод GCC с оптимизацией, кажется, что он не беспокоился об исследовании:
.LC0:
.string "formatting hard disk drive!"
never_called():
mov edi, OFFSET FLAT:.LC0
jmp puts
set_foo():
mov QWORD PTR foo[rip], OFFSET FLAT:never_called()
ret
main:
sub rsp, 8
call [QWORD PTR foo[rip]]
xor eax, eax
add rsp, 8
ret
Выполнение этой программы приводит к сбою (сбою сегментации), но если вы вызываете set_foo
в другой блок перевода до того, как основной будет выполнен, то эта программа больше не будет демонстрировать поведение undefined.
Все это может смутно измениться, поскольку все больше и больше оптимизируются, поэтому не полагайтесь на предположение, что ваш компилятор позаботится о коде, содержащем поведение undefined, он может просто повредить вас (и отформатировать ваши жесткий диск для реального!)
Я рекомендую вам читать Что каждый программист C должен знать о undefined Поведение и Руководство по undefined Поведение на C и С++, обе статьи очень информативны и могут помочь вам понять состояние дел.