Означает ли существование такого оператора в данной программе, что вся программа не определена или что поведение становится неопределенным только после того, как поток управления попадает в этот оператор?
Ни то, ни другое. Первое условие слишком сильное, а второе слишком слабое.
Доступ к объектам иногда является последовательным, но стандарт описывает поведение программы вне времени. Данвил уже цитировал:
если любое такое выполнение содержит неопределенную операцию, настоящий международный стандарт не налагает никаких требований на реализацию, выполняющую эту программу с этим вводом (даже в отношении операций, предшествующих первой неопределенной операции)
Это можно интерпретировать:
Если выполнение программы приводит к неопределенному поведению, тогда вся программа имеет неопределенное поведение.
Итак, недостижимый оператор с UB не дает программе UB. Оператор достижимости, который (из-за значений входных данных) никогда не достигается, не дает программе UB. Вот почему ваше первое условие слишком сильное.
Теперь компилятор не может вообще сказать, что есть UB. Таким образом, чтобы позволить оптимизатору переупорядочивать операторы с потенциальным UB, который будет переупорядочен, если их поведение будет определено, необходимо разрешить UB «вернуться назад во времени» и пойти не так до предыдущей точки последовательности (или в C Терминология ++ 11, чтобы UB влиял на вещи, которые упорядочены до вещи UB). Следовательно, ваше второе условие слишком слабое.
Главный пример этого - когда оптимизатор полагается на строгое алиасинг. Весь смысл строгих правил псевдонима состоит в том, чтобы позволить компилятору переупорядочивать операции, которые нельзя было бы правильно переупорядочить, если бы было возможно, что рассматриваемые указатели являются псевдонимами одной и той же памяти. Таким образом, если вы используете незаконные указатели псевдонимов, а UB действительно встречается, то это может легко повлиять на оператор «перед» оператором UB. Что касается абстрактной машины, то оператор UB еще не был выполнен. Что касается фактического объектного кода, он был частично или полностью выполнен. Но стандарт не пытается подробно описать, что значит для оптимизатора переупорядочить операторы, или каковы последствия этого для UB. Он просто дает лицензию на реализацию пойти не так, как только захочет.
Вы можете думать об этом так: «У UB есть машина времени».
В частности, чтобы ответить на ваши примеры:
- Поведение не определено, только если прочитано 3.
- Компиляторы могут исключать и устраняют код как мертвый, если базовый блок содержит операцию, которая точно не определена. Они разрешены (и я предполагаю, что да) в случаях, которые не являются базовым блоком, но когда все ветки ведут к UB. Этот пример не является кандидатом, если
PrintToConsole(3)
не известно, что он обязательно вернется. Это могло вызвать исключение или что-то еще.
Примером, аналогичным вашему второму, является опция gcc -fdelete-null-pointer-checks
, которая может принимать такой код (я не проверял этот конкретный пример, считаю его иллюстративным для общей идеи):
void foo(int *p) {
if (p) *p = 3;
std::cout << *p << '\n';
}
и измените его на:
*p = 3;
std::cout << "3\n";
Зачем? Поскольку if p
имеет значение NULL, код все равно имеет UB, поэтому компилятор может предположить, что он не равен NULL, и соответственно оптимизировать. Ядро Linux споткнулось об этом ( https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897 ) по сути потому, что оно работает в режиме, в котором разыменование нулевого указателя не должно быть UB, ожидается, что это приведет к определенному аппаратному исключению, которое ядро может обработать. Когда оптимизация включена, gcc требует использования-fno-delete-null-pointer-checks
, чтобы предоставить эту нестандартную гарантию.
PS Практический ответ на вопрос "когда поражает неопределенное поведение?" это «за 10 минут до того, как вы собирались уехать на день».