Чи означає існування такого твердження в даній програмі, що вся програма невизначена, або що поведінка стає невизначеною лише після того, як потік управління потрапляє в це твердження?
Ні того, ні іншого. Перша умова занадто сильна, а друга занадто слабка.
Доступ до об’єктів іноді секвенується, але стандарт описує поведінку програми поза часом. Данвіл вже цитував:
якщо будь-яке таке виконання містить невизначену операцію, цей Міжнародний стандарт не встановлює вимог до реалізації, що виконує цю програму з цим входом (навіть щодо операцій, що передують першій невизначеній операції)
Це можна інтерпретувати:
Якщо виконання програми дає невизначену поведінку, то вся програма має невизначену поведінку.
Отже, недосяжне твердження з 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";
Чому? Оскільки якщо p
значення null, тоді в коді так чи інакше є UB, тому компілятор може припустити, що він не є null і відповідно оптимізувати. Лінукс ядро спіткнувся це ( https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897 ) , по суті , так як він працює в режимі , коли разименованія покажчика NULL НЕ передбачається як UB, очікується, що це призведе до визначеного апаратного винятку, який ядро може обробити. Коли ввімкнено оптимізацію, gcc вимагає використання -fno-delete-null-pointer-checks
, щоб надати цю нестандартну гарантію.
PS Практична відповідь на питання "коли вражає невизначена поведінка?" "10 хвилин до того, як ви планували виїхати на день".