У який момент циклу переповнення цілих чисел стає невизначеною поведінкою?


86

Це приклад для ілюстрації мого запитання, яке включає набагато складніший код, який я не можу розмістити тут.

#include <stdio.h>
int main()
{
    int a = 0;
    for (int i = 0; i < 3; i++)
    {
        printf("Hello\n");
        a = a + 1000000000;
    }
}

Ця програма містить невизначену поведінку на моїй платформі, оскільки aвона переповнюється в 3-му циклі.

Чи це спричиняє невизначену поведінку всієї програми або лише після того, як насправді відбувається переповнення ? Чи може компілятор потенційно працювати, що a буде переповнюватися , тому він може оголосити весь цикл невизначеного і не турбувати , щоб запустити printfs , навіть якщо вони все відбудуться до переливу?

(Позначені C та C ++, хоча і відрізняються, бо мене цікавлять відповіді для обох мов, якщо вони різні.)


7
Цікаво, чи міг компілятор опрацювати те, що aне використовується (за винятком самого обчислення), і просто видалитиa
4386427

12
Вам може сподобатися My Little Optimizer: Невизначена поведінка - це магія від CppCon цього року. Вся справа в тому, що можуть виконувати компілятори оптимізації на основі невизначеної поведінки.
TartanLlama



Відповіді:


108

Якщо вас цікавить чисто теоретична відповідь, стандарт С ++ дозволяє невизначену поведінку "подорожувати у часі":

[intro.execution]/5: Відповідна реалізація, що виконує добре сформовану програму, повинна виробляти таку ж спостережувану поведінку, як одне з можливих виконання відповідного екземпляра абстрактної машини з тією ж програмою та однаковим входом. Однак, якщо будь-яке таке виконання містить невизначену операцію, цей Міжнародний стандарт не встановлює вимог до реалізації, що виконує цю програму з цим входом (навіть щодо операцій, що передують першій невизначеній операції)

Таким чином, якщо ваша програма містить невизначену поведінку, то поведінка всієї вашої програми невизначена.


4
@KeithThompson: Але тоді сама sneeze()функція не визначена ні для чого з класу Demon(серед яких носовий різновид є підкласом), роблячи все це круговим чином.
Себастьян Ленартович

1
Але printf може не повернутися, тому перші два раунди визначені, тому що доки не буде зроблено, незрозуміло, що колись буде UB. Дивіться stackoverflow.com/questions/23153445/…
usr

1
Тому компілятор технічно в межах своїх прав на викид «NOP» для ядра Linux (оскільки код початкового завантаження залежить від невизначеного поведінки): blog.regehr.org/archives/761
Crashworks

3
@Crashworks І ось чому Linux пишеться та компілюється як непереносимий C. (тобто надмножина C, що вимагає певного компілятора з певними параметрами, наприклад -fno-strict-aliasing)
user253751

3
@usr Я сподіваюсь, це визначено, якщо printfне повертається, але якщо printfзбирається повернутися, тоді невизначена поведінка може спричинити проблеми перед printfвикликом. Отже, подорож у часі. printf("Hello\n");а потім наступний рядок компілюється якundoPrintf(); launchNuclearMissiles();
user253751

31

Спочатку дозвольте мені виправити заголовок цього запитання:

Невизначена поведінка не є (зокрема) сферою виконання.

Невизначена поведінка впливає на всі етапи: компіляцію, зв’язування, завантаження та виконання.

Деякі приклади, щоб це закріпити, майте на увазі, що жоден розділ не є вичерпним:

  • компілятор може припустити, що частини коду, які містять невизначену поведінку, ніколи не виконуються, і, отже, припустити, що шляхи виконання, які призвели б до них, є мертвим кодом. Подивіться, що кожен програміст на C повинен знати про невизначену поведінку не хто інший, як Кріс Латтнер.
  • лінкер може припустити, що за наявності кількох визначень слабкого символу (розпізнаного за іменем), всі визначення ідентичні завдяки Правилу одного визначення
  • завантажувач (якщо ви використовуєте динамічні бібліотеки) може припустити те саме, вибравши таким чином перший знайдений символ; це зазвичай (ab) використовується для перехоплення дзвінків за допомогою LD_PRELOADхитрощів на Unix
  • виконання може не вдатися (SIGSEV), якщо ви використовуєте звисаючі вказівники

Це те, що так страшно щодо невизначеної поведінки: майже неможливо заздалегідь передбачити, яка саме поведінка відбуватиметься, і це прогнозування слід переглядати при кожному оновленні ланцюжка інструментів, що лежить в основі ОС, ...


Я рекомендую переглянути це відео Майкла Спенсера (розробник LLVM): CppCon 2016: Мій маленький оптимізатор: Невизначена поведінка - це магія .


3
Це те, що мене турбує. У моєму реальному коді він складний, але у мене може бути випадок, коли він завжди переповнюється. І мене насправді це не хвилює, але я переживаю, що це вплине на "правильний" код. Очевидно, мені потрібно це виправити, але виправлення вимагає розуміння :)
jcoder

8
@jcoder: Тут є одна важлива втеча. Компілятору не дозволяється вгадувати вхідні дані. Поки існує хоча б один вхід, для якого не існує невизначеної поведінки, компілятор повинен переконатися, що саме цей вхід все ще видає правильний результат. Усі страшні розмови про небезпечні оптимізації стосуються лише УБ, яких не уникнути . Практично кажучи, якщо б ви використовували argcяк підрахунок циклів, справа argc=1не створює UB, і компілятор буде змушений це обробляти.
MSalters

@jcoder: У цьому випадку це не мертвий код. Однак компілятор міг би бути достатньо розумним, щоб зробити висновок, який iне можна збільшити більше, ніж Nразів, а отже, що його значення обмежене.
Matthieu M.

4
@jcoder: Якщо f(good);робить щось X і f(bad);викликає невизначену поведінку, тоді програма, яка просто викликає f(good);, гарантовано робить X, але f(good); f(bad);не гарантує, що робить X.

4
@Hurkyl, що цікавіше, якщо ваш код є if(foo) f(good); else f(bad);, розумний компілятор відкине порівняння та результат і безумовний foo(good).
Джон Дворжак,

28

Агресивно оптимізуючий компілятор C або C ++, націлений на 16 біт, intбуде знати, що поведінка при додаванні 1000000000до intтипу невизначена .

Обидва стандарти дозволяють робити все, що завгодно, що може включати видалення всієї програми, залишаючи int main(){}.

Але як щодо великих ints? Я ще не знаю компілятора, який би це робив (і я не є експертом у розробці компілятора C і C ++ будь-якими способами), але я думаю, що колись компілятор, орієнтований на 32-бітну версію intабо більше, зрозуміє, що цикл нескінченна ( iне змінюється) і тому aв кінцевому підсумку переповнення. Отже, ще раз, він може оптимізувати вихід до int main(){}. Суть, яку я намагаюся сказати тут, полягає в тому, що в міру того, як оптимізація компілятора стає поступово більш агресивною, все більше і більше невизначених конструкцій поведінки проявляються несподівано.

Той факт, що ваш цикл нескінченний, сам по собі не визначений, оскільки ви пишете на стандартний вивід у тілі циклу.


3
Чи дозволено стандартом робити що завгодно ще до того, як виявиться невизначена поведінка? Де це сказано?
jimifiki

4
чому 16 біт? Я думаю, OP шукає 32-бітне переповнене підписом.
4386427

8
@jimifiki У стандартному. C ++ 14 (N4140) 1.3.24 "невизначена поведінка = поведінка, до якої цей міжнародний стандарт не вимагає вимог". Плюс довга примітка, яка детально розроблена. Але справа в тому, що не поведінка "твердження" не визначена, це поведінка програми. Це означає, що поки UB ініціюється правилом стандарту (або відсутністю правила), стандарт перестає застосовуватися для програми в цілому. Тож будь-яка частина програми може поводитись як хоче.
Енджу більше не пишається SO

5
Перше твердження помилкове. Якщо intє 16-розрядним, додавання відбуватиметься в long(оскільки літеральний операнд має тип long) там, де він чітко визначений, а потім буде перетворено за допомогою визначеного реалізацією перетворення назад у int.
R .. GitHub СТОП ДОПОМОГАЙ ЛЕД

2
@usr поведінка користувача printfвизначається стандартом завжди повертатись
MM

11

Технічно, за стандартом С ++, якщо програма містить невизначену поведінку, поведінка всієї програми, навіть під час компіляції (ще до того, як програма буде навіть виконана), невизначена.

На практиці, оскільки компілятор може припустити (як частина оптимізації), що переповнення не відбудеться, принаймні поведінка програми на третій ітерації циклу (припускаючи 32-розрядну машину) буде невизначеною, хоча це ймовірно, ви отримаєте правильні результати до третьої ітерації. Однак, оскільки поведінка всієї програми технічно невизначена, ніщо не заважає програмі генерувати абсолютно неправильний результат (включаючи відсутність вихідних даних), збій під час виконання в будь-який момент під час виконання або навіть невдачу компіляції (оскільки невизначена поведінка поширюється на час компіляції).

Невизначена поведінка надає компілятору більше можливостей для оптимізації, оскільки вони усувають певні припущення щодо того, що повинен робити код. При цьому програми, які спираються на припущення, що стосуються невизначеної поведінки, не гарантовано працюватимуть належним чином. Таким чином, ви не повинні покладатися на якусь конкретну поведінку, яка вважається невизначеною відповідно до стандарту C ++.


Що робити, якщо частина UB знаходиться в межах зони if(false) {}дії? Чи отруює це всю програму через те, що компілятор припускає, що всі гілки містять ~ чітко визначені частини логіки, і таким чином працює на неправильних припущеннях?
mlvljr

1
Стандарт не встановлює жодних вимог щодо невизначеної поведінки, тому теоретично так, він отруює всю програму. Однак на практиці будь-який оптимізуючий компілятор, швидше за все, просто видалить мертвий код, тому, ймовірно, це не вплине на виконання. Тим не менше, ти все одно не повинен покладатися на таку поведінку.
bwDraco

Добре знати, дякую :)
mlvljr

9

Щоб зрозуміти, чому невизначена поведінка може «подорожувати у часі», як адекватно висловився @TartanLlama , давайте подивимось на правило «як би»:

1.9 Виконання програми

1 Семантичні описи в цьому міжнародному стандарті визначають параметризовану недетерміновану абстрактну машину. Цей міжнародний стандарт не встановлює вимог щодо структури відповідних реалізацій. Зокрема, їм не потрібно копіювати або наслідувати структуру абстрактної машини. Швидше, відповідні реалізації вимагають імітувати (лише) спостережувану поведінку абстрактної машини, як пояснено нижче.

Завдяки цьому ми могли б розглядати програму як "чорний ящик" із входом і виходом. Вхідними даними можуть бути користувацькі дані, файли та багато іншого. Результатом є "спостережувана поведінка", згадана у стандарті.

Стандарт визначає лише відображення між входом і виходом, нічого іншого. Він робить це, описуючи "приклад чорного ящика", але прямо говорить, що будь-який інший чорний ящик з тим самим відображенням є однаково дійсним. Це означає, що вміст чорної скриньки не має значення.

З огляду на це, не мало б сенсу стверджувати, що невизначена поведінка відбувається в певний момент. У зразку реалізації чорного ящика ми могли б сказати, де і коли це відбувається, але фактично чорний ящик може бути чимось зовсім іншим, тому ми не можемо сказати, де і коли це відбувається. Теоретично, компілятор міг, наприклад, вирішити перерахувати всі можливі вхідні дані та попередньо обчислити отримані результати. Тоді невизначена поведінка могла б статися під час компіляції.

Невизначена поведінка - це відсутність відображення між входом і виходом. Програма може мати невизначену поведінку для одних входів, але визначена поведінка для інших. Тоді відображення між введенням і виходом просто неповне; є вхідні дані, для яких не існує зіставлення з вихідними даними.
Програма у питанні має невизначену поведінку для будь-якого вводу, тому відображення порожнє.


6

Припускаючи int, що 32-розрядна, невизначена поведінка трапляється на третій ітерації. Отже, якщо, наприклад, цикл був лише умовно доступний, або міг бути умовно завершений до третьої ітерації, не було б невизначеної поведінки, якщо третя ітерація фактично не досягнута. Однак у випадку невизначеної поведінки весь вихід програми є невизначеним, включаючи вихід, який є "в минулому" щодо виклику невизначеної поведінки. Наприклад, у вашому випадку це означає, що немає гарантії побачити 3 повідомлення "Привіт" у вихідних даних.


6

Відповідь TartanLlama правильна. Невизначена поведінка може статися в будь-який час, навіть під час компіляції. Це може здатися абсурдом, але це ключова особливість, яка дозволяє компіляторам робити те, що їм потрібно. Бути компілятором не завжди легко. Щоразу потрібно робити саме те, що сказано в специфікації. Однак іноді буває жахливо важко довести, що відбувається певна поведінка. Якщо ви пам'ятаєте проблему зупинки, досить тривіально розробити програмне забезпечення, для якого ви не можете довести, чи воно завершує або входить у нескінченний цикл, коли подається певний вхід.

Ми могли б зробити компілятори песимістами і постійно компілювати, боячись, що наступна інструкція може бути однією з таких зупиняючих проблем, як проблеми, але це не розумно. Натомість ми даємо компілятору пропуск: щодо цих тем "невизначеної поведінки" вони звільняються від будь-якої відповідальності. Невизначена поведінка складається з усіх видів поведінки, які є настільки витонченими, що у нас виникають проблеми з відокремленням їх від справді неприємно-підлих проблем зупинки та нічого іншого.

Є приклад, який я люблю публікувати, хоча, зізнаюся, я втратив джерело, тому доводиться перефразовувати. Це було з певної версії MySQL. У MySQL вони мали круговий буфер, який заповнювався наданими користувачем даними. Вони, звичайно, хотіли переконатися, що дані не переповнюють буфер, тому вони мали перевірку:

if (currentPtr + numberOfNewChars > endOfBufferPtr) { doOverflowLogic(); }

Це виглядає досить осудно. Однак що, якщо числоOfNewChars справді велике і переповнюється? Потім він обертається і стає покажчиком менше endOfBufferPtr, тому логіка переповнення ніколи не викликається. Тож вони додали другу перевірку, перед тим:

if (currentPtr + numberOfNewChars < currentPtr) { detectWrapAround(); }

Схоже, ви подбали про помилку переповнення буфера, так? Однак була подана помилка про те, що цей буфер переповнений у певній версії Debian! Ретельне дослідження показало, що ця версія Debian була першою, хто використав особливо криваву версію gcc. У цій версії gcc компілятор визнав, що currentPtr + numberOfNewChars ніколи не може бути меншим покажчиком, ніж currentPtr, оскільки переповнення покажчиків є невизначеною поведінкою! Цього було достатньо, щоб gcc оптимізував всю перевірку, і раптом ви не були захищені від переповнення буфера, хоча ви написали код для перевірки!

Це була специфічна поведінка. Все було законно (хоча з того, що я чув, gcc відкотив цю зміну в наступній версії). Це не те, що я вважав би інтуїтивною поведінкою, але якщо трохи розтягнути свою уяву, легко зрозуміти, як незначний варіант цієї ситуації може стати зупинкою для компілятора. Через це автори специфікацій зробили це "Невизначеною поведінкою" і заявили, що компілятор може робити абсолютно все, що завгодно.


Я не вважаю особливо дивовижними компіляторами, які іноді поводяться так, ніби підписана арифметика виконується для типів, діапазон яких виходить за межі "int", особливо враховуючи, що навіть при простому генерації коду на x86 бувають випадки, коли це робить ефективніше, ніж скорочення проміжних результати. Що ще дивовижніше, коли переповнення впливає на інші обчислення, що може статися в gcc, навіть якщо код зберігає добуток двох значень uint16_t у uint32_t - операція, яка не повинна мати правдоподібних причин діяти несподівано в несанітарній побудові.
supercat

Звичайно, правильною буде перевірка за if(numberOfNewChars > endOfBufferPtr - currentPtr)умови, що numberOfNewChars ніколи не може бути від’ємною, а currentPtr завжди вказує на десь у буфері, де вам навіть не потрібна смішна перевірка «обгортання». (Я не думаю, що код, який ви надали, сподівається працювати в круговому буфері - ви
перепустили

@ Random832 Я залишив тону. Я намагався процитувати більший контекст, але, оскільки я втратив своє джерело, виявив, що перефразування контексту спричинило мені більші проблеми, тому я залишаю це поза увагою. Мені дійсно потрібно знайти цей звіт про помилку, щоб я міг навести його належним чином. Це справді потужний приклад того, як ви можете думати, що написали код в один бік, і змусити його компілювати зовсім по-іншому.
Корт Аммон

Це моя найбільша проблема з невизначеною поведінкою. Це робить часом неможливим написання правильного коду, і коли компілятор виявляє його, за замовчуванням не повідомляє вам, що це викликано невизначеною поведінкою. У цьому випадку користувач просто хоче зробити арифметику - вказівник чи ні, - і вся їхня напружена робота над написанням захищеного коду була скасована. Повинний бути принаймні спосіб анотувати розділ коду, щоб сказати - ніяких вигадливих оптимізацій тут. C / C ++ використовується у занадто багатьох критичних областях, щоб дозволити продовжувати цю небезпечну ситуацію на користь оптимізації
Джон Макграт,

4

Окрім теоретичних відповідей, практичним спостереженням було б те, що протягом тривалого часу компілятори застосовували різні перетворення на петлях, щоб зменшити обсяг роботи, виконаної в них. Наприклад, враховуючи:

for (int i=0; i<n; i++)
  foo[i] = i*scale;

компілятор може перетворити це на:

int temp = 0;
for (int i=0; i<n; i++)
{
  foo[i] = temp;
  temp+=scale;
}

Таким чином зберігається множення з кожною ітерацією циклу. Додаткова форма оптимізації, яку компілятори адаптували з різним ступенем агресивності, перетворила б це на:

if (n > 0)
{
  int temp1 = n*scale;
  int *temp2 = foo;
  do
  {
    temp1 -= scale;
    *temp2++ = temp1;
  } while(temp1);
}

Навіть на машинах із тихим обтіканням при переповненні це могло б вийти з ладу, якщо було якесь число менше n, яке при множенні на масштаб дало б 0. Це також могло б перетворитися на нескінченний цикл, якщо масштаб читався з пам'яті більше одного разу і щось неочікувано змінив своє значення (у будь-якому випадку, коли "масштаб" може змінити середину циклу без виклику UB, компілятору не буде дозволено виконувати оптимізацію).

Хоча більшість таких оптимізацій не матиме жодних проблем у випадках, коли два короткі типи без знака перемножуються, отримуючи значення, яке знаходиться між INT_MAX + 1 та UINT_MAX, gcc має деякі випадки, коли таке множення всередині циклу може призвести до раннього виходу циклу . Я не помічав такої поведінки, що випливає з інструкцій порівняння у згенерованому коді, але це спостерігається у випадках, коли компілятор використовує переповнення, щоб зробити висновок, що цикл може виконуватися щонайбільше 4 або менше разів; він не за замовчуванням генерує попередження у випадках, коли одні входи спричиняють UB, а інші - ні, навіть якщо його висновки спричиняють ігнорування верхньої межі циклу.


4

Невизначена поведінка, за визначенням, є сірою зоною. Ви просто не можете передбачити, що він буде чи ні, - ось що означає «невизначена поведінка» .

З незапам'ятних часів програмісти завжди намагалися врятувати залишки визначеності із невизначеної ситуації. У них є якийсь - то код , який вони дійсно хочете використовувати, але виявляється невизначеним, тому вони намагаються стверджувати: «Я знаю , що не визначене, але , звичайно , це не буде, в гіршому випадку, роблять це або це, він ніколи не робити , що . " І іноді ці аргументи є більш-менш правильними - але часто вони помиляються. І оскільки компілятори стають розумнішими та розумнішими (або, дехто може сказати, підступнішими та підступнішими), межі питання постійно змінюються.

Отже, якщо ви хочете написати код, який гарантовано буде працювати і який буде працювати довгий час, є лише один вибір: уникайте невизначеної поведінки будь-якою ціною. По-справжньому, якщо ти балуєшся цим, воно повернеться до тебе.


і все-таки, ось у чому річ ... компілятори можуть використовувати невизначену поведінку для оптимізації, але ВЗАГАЛІ ВАМ НЕ РОЗКАЗУЮТЬ. Отже, якщо ми маємо цей чудовий інструмент, який вам доведеться уникати робити X за всяку ціну, чому компілятор не може дати вам попередження, щоб ви могли це виправити?
Jason S

1

Одне, що ваш приклад не враховує, це оптимізація. aвстановлюється в циклі, але ніколи не використовується, і оптимізатор міг би це вирішити. Таким чином, правомірно для оптимізатора aповністю відкинути , і в такому випадку вся невизначена поведінка зникає, як жертва буджума.

Однак, звичайно, це саме не визначено, оскільки оптимізація невизначена. :)


1
Немає підстав розглядати питання оптимізації, визначаючи, чи поведінка невизначена.
Кіт Томпсон,

2
Той факт, що програма поводиться так, як можна було б припустити, не означає, що невизначена поведінка "зникає". Поведінка все ще невизначена, і ви просто покладаєтесь на удачу. Сам факт того, що поведінка програми може змінюватися залежно від параметрів компілятора, є вагомим показником того, що поведінка невизначена.
Джордан Мело

@JordanMelo Оскільки в багатьох попередніх відповідях обговорювалось питання оптимізації (і OP спеціально про це запитував), я згадав про функцію оптимізації, яку жодна попередня відповідь не охоплювала. Я також зазначив, що навіть незважаючи на те, що оптимізація може її усунути, покладання на оптимізацію для того, щоб працювати певним чином, знову не визначено. Я точно не рекомендую! :)
Грем

@KeithThompson Звичайно, але OP спеціально запитав про оптимізацію та її вплив на невизначену поведінку, яку він побачить на своїй платформі. Ця специфічна поведінка може зникнути залежно від оптимізації. Як я вже сказав у своїй відповіді, невизначеність цього не робить.
Грем

0

Оскільки це питання є подвійними мітками C і C ++, я спробую розглянути обидва. C і C ++ тут ​​застосовують різні підходи.

У C реалізація повинна бути в змозі довести, що буде викликана невизначена поведінка, щоб ставитися до всієї програми так, ніби вона має невизначену поведінку. У прикладі OP здається тривіальним для компілятора довести це, і тому це як би вся програма була невизначена.

Це ми можемо побачити із звіту про дефекти 109, який на суть запитує:

Якщо, однак, стандарт C визнає окреме існування "невизначених значень" (чиє просто створення не передбачає повністю "невизначеної поведінки"), то особа, яка проводить тестування компілятора, може написати такий тест, наприклад, наступний, і він / вона також може очікувати (або, можливо, вимагати), щоб відповідна реалізація, як мінімум, компілювала цей код (і, можливо, також дозволяла йому виконуватися) без "збою".

int array1[5];
int array2[5];
int *p1 = &array1[0];
int *p2 = &array2[0];

int foo()
{
int i;
i = (p1 > p2); /* Must this be "successfully translated"? */
1/0; /* Must this be "successfully translated"? */
return 0;
}

Тож питання в підсумку таке: Чи повинен вищевказаний код бути "успішно перекладеним" (що б це не означало)? (Див. Виноску, додану до підпункту 5.1.1.3.)

і відповідь була така:

Стандарт C використовує термін "невизначена вартість", а не "невизначена вартість". Використання невизначеного значення об'єкта призводить до невизначеної поведінки. У виносці до підпункту 5.1.1.3 зазначається, що реалізація може виробляти будь-яку кількість діагностик, якщо діюча програма все ще правильно перекладена. Якщо вираз, евакуація якого призведе до невизначеної поведінки, з'являється в контексті, де потрібен постійний вираз, програма, що містить, не відповідає строго. Крім того, якщо кожне можливе виконання даної програми призведе до невизначеної поведінки, дана програма не відповідає строго. Відповідна реалізація не повинна не перекладати строго відповідну програму просто тому, що якесь можливе виконання цієї програми призведе до невизначеної поведінки. Оскільки foo ніколи не може бути викликаний, наведений приклад повинен бути успішно перекладений відповідною реалізацією.

У C ++ підхід виглядає більш спокійним і передбачає, що програма має невизначену поведінку, незалежно від того, може реалізація статично це довести чи ні.

У нас є [intro.abstrac] p5, який говорить:

Відповідна реалізація, що виконує добре сформовану програму, повинна виробляти таку ж спостережувану поведінку, як одне з можливих виконання відповідного екземпляра абстрактної машини з тією ж програмою та однаковим входом. Однак, якщо будь-яке таке виконання містить невизначену операцію, цей документ не встановлює вимоги до реалізації, що виконує цю програму з цим входом (навіть щодо операцій, що передують першій невизначеній операції).


Той факт, що виконання функції може викликати UB, може впливати на поведінку програми, якщо їй надано певний вхід, якщо хоча б одне можливе виконання програми, якщо вказано, що вхід викликає UB. Той факт, що виклик функції викликає UB, не заважає програмі визначити поведінку, коли вона подається на вхід, що не дозволяє викликати функцію.
supercat

@supercat Я вважаю, що саме так я відповідаю, принаймні для C.
Шафік Ягмур

Я думаю, те саме стосується і тексту, що цитується, на C ++, оскільки фраза "Будь-яке таке виконання" стосується способів, які програма може виконати з певним даним входом. Якщо певне введення не може призвести до виконання функції, я не бачу в цитованому тексті нічого, що б свідчило про те, що щось у такій функції призведе до UB.
supercat

-2

Основна відповідь - неправильне (але поширене) помилкове уявлення:

Невизначена поведінка є час виконання нерухомості *. Це НЕ МОЖЕ "подорожувати у часі"!

Деякі операції визначені (за стандартом), що мають побічні ефекти і не можуть бути оптимізовані. Операції, що виконують введення-виведення або доступvolatile цієї категорії належать змінні .

Однак є застереження: UB може бути будь-якою поведінкою, включаючи поведінку, яка скасовує попередні операції. У деяких випадках це може мати подібні наслідки для оптимізації попереднього коду.

Насправді це узгоджується з цитатою у верхній відповіді (наголос мій):

Відповідна реалізація, що виконує добре сформовану програму, повинна виробляти таку ж спостережувану поведінку, як одне з можливих виконання відповідного екземпляра абстрактної машини з тією ж програмою та однаковим входом.
Однак, якщо будь-яке таке виконання містить невизначену операцію, цей Міжнародний стандарт не встановлює вимог до реалізації, що виконує цю програму з цим входом (навіть щодо операцій, що передують першій невизначеній операції).

Так, ця цитата робить говорить «навіть не стосовно операцій , що передують першому невизначеною операції» , але зверніть увагу , що це саме про те , що код в даний час виконується , а не просто компіляції.
Зрештою, невизначена поведінка, якої насправді не досягнуто, нічого не робить, і для того, щоб рядок, що містить UB, був фактично досягнутий, код, що передує йому, повинен виконатись першим!

Так що так, після виконання UB будь-які ефекти попередніх операцій стають невизначеними. Але поки цього не станеться, виконання програми є чітко визначеним.

Однак зауважте, що всі виконання програми, що призводять до цього, можуть бути оптимізовані до еквівалентних програм, включаючи будь-які, які виконують попередні операції, але потім скасовують їх ефекти. Отже, попередній код може бути оптимізований, коли це буде еквівалентно скасуванню їхніх ефектів ; інакше не може. Див. Приклад нижче.

* Примітка: Це не суперечить UB, що виникає під час компіляції . Якщо компілятор дійсно може довести , що UB код буде завжди виконуватися для всіх входів, то UB може поширюватися на час компіляції. Однак для цього потрібно знати, що весь попередній код врешті-решт повертається , що є суворою вимогою. Знову ж, див. Нижче приклад / пояснення.


Щоб зробити це конкретним, зверніть увагу, що наступний код повинен друкуватись fooі чекати вашого введення, незалежно від будь-якої невизначеної поведінки, що слідує за ним:

printf("foo");
getchar();
*(char*)1 = 1;

Однак також зауважте, що немає жодної гарантії, що fooзалишиться на екрані після виникнення UB, або що введений вами символ більше не буде у вхідному буфері; обидві ці операції можна "скасувати", що має подібний ефект до UB "подорож у часі".

Якби getchar()рядка там не було, було б законно оптимізувати рядки тоді і тільки тоді , коли це було б неможливо відрізнити від виводу, fooа потім " відмовитись " від цього.

Чи не можна буде розрізнити ці два, цілком залежатиме від реалізації (тобто від вашого компілятора та стандартної бібліотеки). Наприклад, чи можна printf заблокувати ваш потік тут, очікуючи, поки інша програма прочитає вихідні дані? Або повернеться негайно?

  • Якщо вона може заблокувати тут, тоді інша програма може відмовитись прочитати повний результат, і вона може ніколи не повернутися, і, отже, UB може ніколи не відбутися.

  • Якщо він може негайно повернутися сюди, тоді ми знаємо, що він повинен повернутися, і тому його оптимізацію абсолютно неможливо відрізнити від його виконання, а потім скасування його наслідків.

Звичайно, оскільки компілятор знає, яка поведінка допустима для його конкретної версії printf, він може відповідно оптимізувати і, отже, printfможе бути оптимізований в деяких випадках, а не в інших. Але, знову ж таки, виправдання полягає в тому, що це неможливо відрізнити від UB, який не робив попередніх операцій, а не в тому, що попередній код "отруєний" через UB.


1
Ви абсолютно неправильно читаєте стандарт. Там сказано, що поведінка під час виконання програми невизначена. Період. Ця відповідь на 100% помилкова. Стандарт дуже чіткий - запуск програми з входом, яка виробляє UB у будь-який момент наївного потоку виконання, не визначений.
Девід Шварц

@DavidSchwartz: Якщо ви дотримуєтесь свого тлумачення до його логічних висновків, ви повинні зрозуміти, що це не має логічного сенсу. Введення - це не те, що повністю визначається під час запуску програми. Вхід в програму (навіть сама її наявність ) у будь-якому заданому рядку може залежати від усіх побічних ефектів програми до цього рядка. Таким чином, програма не може уникнути побічних ефектів, які виникають перед лінією UB, оскільки це вимагає взаємодії з навколишнім середовищем і, отже, впливає на те, чи буде лінія UB досягнута чи ні в першу чергу.
user541686

3
Це не має значення. Дійсно. Знову ж, вам просто бракує фантазії. Наприклад, якщо компілятор може сказати, що жоден сумісний код не може розрізнити, він може перемістити код, який є UB, таким чином, що частина, яка виконується UB, виконується до результатів, які ви наївно очікуєте бути "попередніми".
Девід Шварц

2
@Mehrdad: Можливо, кращим способом сказати щось було б сказати, що UB не може подорожувати в часі за останню точку, де щось могло статися в реальному світі, що визначило б поведінку. Якщо реалізація може визначити, перевіряючи вхідні буфери, що жоден із наступних 1000 викликів getchar () не може заблокувати, а також може визначити, що UB відбудеться після 1000-го виклику, не буде потрібно виконувати жоден із дзвінки. Якщо, проте, реалізація повинна вказати, що виконання не пройде getchar (), поки всі попередні результати не отримають ...
supercat

2
... було доставлено на термінал з 300 бодами, і що будь-який елемент керування C, який виникає раніше, змусить getchar () підняти сигнал, навіть якщо в буфері перед ним були інші символи, тоді така реалізація не може перемістити будь-який UB за останній вивід, що передує getchar (). Що складніше - знати, у якому випадку слід очікувати, що компілятор пройде через програміст будь-які поведінкові гарантії, які може запропонувати бібліотечна реалізація, крім передбачених стандартом.
supercat
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.