Неопределенное поведение, вызывающее перемещение во времени
пример в этой статье из блога msdn сделал меня тикер:
Он говорит, что эта функция:
void unwitting(bool door_is_open)
{
if (door_is_open) {
walk_on_in();
} else {
ring_bell();
// wait for the door to open using the fallback value
fallback = value_or_fallback(nullptr);
wait_for_door_to_open(fallback);
}
}
можно оптимизировать в этот:
void unwitting(bool door_is_open)
{
walk_on_in();
}
потому что вызов value_or_fallback(nullptr)
неопределено поведение (это доказано выше).
Теперь я не понимаю вот что: время выполнения входит в неопределенное поведение только тогда, когда оно достигает этой строки. Не должно ли здесь применяться понятие "до / после", в том смысле, что все наблюдаемые эффекты первого абзаца были разрешены до того, как время выполнения войдет в UB?
3 ответов
в рассуждении есть поток.
когда писатель компилятора говорит:мы используем неопределенное поведение для оптимизации программы, есть две разные интерпретации:
- большинство людей слышат: мы определяем неопределенное поведение и решаем, что можем делать все, что захотим ( * )
- автор компилятора имел в виду:мы предполагаем, что неопределенное поведение не возникает
таким образом, в вашем дело:
- разыменование
nullptr
неопределенное поведение
выполнения
value_or_fallback(nullptr)
неопределенное поведение
исполнение else
ветвь-неопределенное поведение
door_is_open
будучи false
неопределенное поведение
и поскольку неопределенного поведения не происходит (программист клянется, что будет следовать условиям использования),door_is_open
обязательно true
и компилятор может elide else
отделение.
(*)меня немного раздражает, что Раймонд Чен действительно сформулировал это таким образом...
верно, что неопределенное поведение может происходить только во время выполнения (например, разыменование указателя, который является нулевым). В других случаях программа может быть статически "плохо сформирована, не требует диагностики" (например, если вы добавляете явную специализацию для шаблона после того, как он уже был использован), что имеет тот же эффект: вы не можете спорить внутри языка, как будет вести себя ваша программа.
компиляторы могут использовать UB для "оптимизации" генерации кода агрессивно. В дело в том, что компилятор видит, что вторая ветвь вызовет UB (я предполагаю, что это известно статически, хотя вы этого не написали), и поэтому он может предположить, что эта ветвь никогда не берется, так как это неразличимо: если вы сделал введите вторую ветвь, тогда поведение будет неопределенным, и это включает в себя поведение, как вы вошли в первую ветвь. Таким образом, компилятор может просто рассматривать весь путь кода, который приводит к UB, как мертвый и удалить он.
нет никакого способа доказать, что что-то не так.
вот что происходит, когда люди пытаются перевести здравый смысл в спецификацию, а затем интерпретировать спецификацию без здравого смысла. По моему личному мнению, это совершенно неправильно, но это то, что делается в процессе стандартизации языка.
по моему личному мнению, компилятор не должен оптимизировать код с неопределенным поведением. Но нынешние постмодернистские компиляторы просто оптимизируют его. А стандарт допускает оба.
логика особое неправильное поведение, о котором вы упомянули, заключается в том, что компилятор работает с ветвями: если что-то не определено в ветви, он отмечает всю ветвь как имеющую неопределенное поведение; и если ветвь имеет неопределенное поведение, она может быть заменена чем угодно.
самое худшее во всем этом то, что новые версии компилятора могут сломать (и сломать) существующий код-либо не компилируя его, либо компилируя его в ерунду. И "существующий код" обычно представляет собой действительно большое количество код.