Чи може код, який ніколи не буде виконаний, викликати невизначену поведінку?


81

Код, який викликає невизначену поведінку (у цьому прикладі ділення на нуль) ніколи не буде виконаний, чи програма все ще невизначена?

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

Отже, будь-які ідеї?


7
Я б сказав, що це не "поведінка", якщо воно ніколи не буде виконане
Кевін,

1
Якщо UB є виконавчим (наприклад, таким) - це не буде. Але я дуже сумніваюся, що стандарт щось про це говорить.
keltar

13
Звучить як питання семантики, а не програмування.
Вубл

14
@Wooble Я не згоден. Фраза " невизначена поведінка" має особливе значення в C / C ++. І це питання пов’язане з деякими іншими ситуаціями, що визначають невизначену поведінку чи ні. Для запису, якщо ви прочитали стандарт C / C ++, ви всюди знайдете фразу невизначеної поведінки .
Ю Хао

10
@Cornstalks: Стандарт C не використовує фразу "викликає невизначену поведінку", тому ви не можете міркувати про стандарт C на основі того, що може означати ця фраза. Використовувати його для опису С недоречно, оскільки це свідчить про те, що „невизначена поведінка” - це річ, така як стіна, на яку ви натрапляєте, якщо виходите за рамки. Насправді, «невизначена поведінка» - це відсутність речі; це кінець меж. Виїжджаючи з чітко визначеного міста, яке є стандартом С, ви знаходитесь у відкритому полі, де можна побудувати що завгодно.
Eric Postpischil

Відповіді:


71

Давайте подивимося, як стандарт С визначає терміни "поведінка" та "невизначена поведінка".

Посилання на проект N1570 стандарту ISO C 2011; Мені невідомі будь-які відповідні відмінності в будь-якому з трьох опублікованих стандартів ISO C (1990, 1999 та 2011).

Розділ 3.4:

поведінка
зовнішній вигляд або дія

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

Розділ 3.4.3:

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

Там сказано " після використання " такої конструкції. Слово "використання" не визначене стандартом, тому ми повертаємось до загальноанглійського значення. Конструкція не "використовується", якщо вона ніколи не виконується.

Під цим визначенням є примітка:

ПРИМІТКА. Можлива невизначена поведінка варіюється від ігнорування ситуації повністю з непередбачуваними результатами, поведінки під час перекладу або виконання програми документально, характерним для середовища (з видачею діагностичного повідомлення або без нього), до припинення перекладу або виконання (із видача діагностичного повідомлення).

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

який, безумовно, може мати невизначену поведінку, не може бути відхилений під час компіляції.

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

Вашим прикладом було:

який ніколи не виконує ділення на 0. Дуже поширеною ідіомою є:

Ділення, безумовно, має невизначену поведінку if y == 0, але ніколи не виконується if y == 0. Поведінка чітко визначена і з тієї самої причини, що і ваш приклад чітко визначена: оскільки потенційна невизначена поведінка насправді ніколи не може відбутися.

(Хіба що INT_MIN < -INT_MAX && x == INT_MIN && y == -1(так, цілочисельний поділ може переповнюватися), але це окрема проблема.)

У коментарі (після видалення) хтось зазначив, що компілятор може оцінювати константні вирази під час компіляції. Що правда, але не актуально в даному випадку, оскільки в контексті

1/0 не є постійним виразом .

Постійне вираз є синтаксичною категорією , яка зводиться до умовно-вираженню (яке НЕ включає завдання і вираз коми). Виробничий константний вираз з'являється в граматиці лише в контексті, який насправді потребує постійного виразу, наприклад, мітки регістру. Тож якщо ви пишете:

тоді 1/0є константним виразом - і таким, який порушує обмеження в 6.6p4: "Кожен константний вираз має оцінювати константу, яка знаходиться в діапазоні репрезентативних значень для свого типу.", тому потрібна діагностика. Але для правої частини завдання не потрібен константний вираз , а лише умовний вираз , тому обмеження на константні вирази не застосовуються. Компілятор може оцінити будь-який вираз, який він здатний виконати під час компіляції, але лише якщо поведінка така ж, як якщо б вона була оцінена під час виконання (або, в контексті if (0), не обчислювалася під час виконання ().

(Те, що виглядає точно як константний вираз , не обов’язково є константним виразом , так само, як, в x + y * z, послідовність x + yне є адитивним виразом через контекст, в якому вона з’являється.)

Що означає виноску в розділі 6.6 N1570, яку я збирався процитувати:

Таким чином, у наступній ініціалізації
static int i = 2 || 1 / 0;
вираз є дійсним цілочисельним постійним виразом зі значенням один.

насправді не стосується цього питання.

Нарешті, є кілька речей, які визначено спричиняти невизначену поведінку, яка не стосується того, що відбувається під час виконання. Додаток J, розділ 2 стандарту С (знову ж, див. Проект N1570 ) перелічує речі, які спричиняють невизначену поведінку, зібрані з решти стандарту. Деякі приклади (я не стверджую, що це вичерпний перелік):

  • Непорожній вихідний файл не закінчується символом нового рядка, якому не передує символ зворотної косої риски, або закінчується частковою ознакою попередньої обробки або коментарем
  • Конкатенація маркера створює послідовність символів, що відповідає синтаксису універсального імені символу
  • Символ, що не знаходиться в базовому наборі вихідних символів, зустрічається у вихідному файлі, за винятком ідентифікатора, константи символів, рядкового літералу, імені заголовка, коментаря або маркера попередньої обробки, який ніколи не перетворюється на маркер
  • Ідентифікатор, коментар, літеральний рядок, символьна константа або назва заголовка містить недійсний багатобайтовий символ або не починається і не закінчується у початковому стані зсуву
  • Один і той самий ідентифікатор має як внутрішній, так і зовнішній зв'язок в одному блоці перекладу

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


2
@EricPostpischil 6.6 / 4 говорить: "Кожен вираз константи має оцінювати константу, яка знаходиться в діапазоні репрезентативних значень для свого типу". Чи не виключить це 1/0з постійного виразу?
Кейсі

2
@EricPostpischil: Я не думаю, що це цілком правильно. Порушення обмеження, як правило, означає, що потрібна діагностика під час компіляції, а не просто те, що щось, що в іншому випадку може бути foo , не є foo . 1/0не є постійним виразом у контексті запитання, оскільки він не аналізується як константний вираз , а просто як умовний вираз, який є частиною виразу призначення . case 1/0:порушить обмеження та вимагатиме діагностики.
Кіт Томпсон,

DR # 109, схоже, вказує на те, що програма не є UB, див. Нову відповідь, яку я щойно опублікував.
ouah

1
Щодо " таким чином ми повертаємось до загальновизнаного англійського значення ". В англійському значенні програма використовує конструкцію, якщо вона присутня в програмі. То чому у вашій відповіді передбачається, що використання конструкції означає виконання конструкції? Ваш висновок не випливає з вашого пояснення!
ikegami

31

Ця стаття обговорює це питання в розділі 2.6:

Автори вважають, що програма визначається тоді, коли guard()не припиняється. Вони також розрізняють поняття "статично невизначене" та "динамічно невизначене", наприклад:

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

11) Приватне листування з членом комітету.

Я б порадив переглянути всю статтю. У сукупності це створює послідовну картину.

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


1
Цей приклад представляє труднощі лише у тому, чи можете ви статично визначити, чи не визначена його поведінка. Коли він виконується (припускаючи, що поведінка guard()не визначена), поведінка невизначена тоді і лише тоді, коли оператор 5 / 0;насправді виконується. (Зверніть увагу, що компілятор міг би законно замінити оцінку 5 / 0викликом abort()або чимось подібним; програма тоді перервала б тоді і лише тоді, коли виконання досягне цієї точки.) Компілятор може відхилити цю програму лише в тому випадку, якщо він може визначити, що guard()завжди буде припинено.
Кіт Томпсон,

@KeithThompson Щоб пояснити статичне / динамічне розрізнення в статті, 5/0 вважається динамічним, оскільки компілятор може генерувати код, який ділиться на нуль: просто генеруйте звичайний код, який ділиться на z після встановлення z на 0. Таким чином, наївний компілятор може генерувати інструкцію ділення. Складний компілятор, який визначає, що guard()не закінчується, не повинен генерувати будь-який код для 5/0. На відміну від цього, немає способу генерувати код для (int)(void)5, не можна просто генерувати код для (int)(void)z, оскільки це теж не правильно. Тож автори вважають, що ...
Паскаль Куок

@KeithThompson… компілятору дозволено відхилити програму if (0) (int)(void)5;через загадку, яку вона представляє наївному компілятору, тоді як недосяжний динамічний UB, такий як if (0) 5 / 0;нешкідливий. Це те, що з’ясувалося в ході їх обговорення з членом комітету, і я бачив подібний аргумент в інших місцях (але, можливо, з того самого джерела, тим більше, що я не пам’ятаю, де це було). На даний момент я переглядаю обґрунтування C99, якщо я побачу будь-яку згадку про це, я повернусь і вкажу на це.
Паскаль Куок

2
(int)(void)5є порушенням обмеження. N1570 6.5.4, що описує оператор приведення: "Обмеження: Якщо ім'я типу не вказує тип порожнечі, ім'я типу має вказувати атомний, кваліфікований або некваліфікований скалярний тип, а операнд повинен мати скалярний тип.". (void)5не має скалярного типу, тому (int)(void)5порушує це обмеження, незалежно від того, чи виконується код, що його містить.
Кіт Томпсон,

@KeithThompson Так, схоже, вони вибрали неправильний приклад, але всередині довгого списку в J.2 є такий, який не є порушенням обмежень, і який, звичайно, є "статичним"? Як щодо тієї старої класики, "Непустий вихідний файл не закінчується символом нового рядка ..."? Не існує поняття доступності, яке стосується цього, але це не є порушенням обмежень, чи не так?
Паскаль Куок

5

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

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


Для прикладу випадку 2 розглянемо, #include "//e"який викликає UB.
Michael Foukarakis

2

Я хотів би взяти останній абзац цієї відповіді: https://stackoverflow.com/a/18384176/694576

... UB - це проблема виконання, а не проблема компіляції ...

Отже, ні, жоден UB не викликається.


2
Ви не повинні вірити всьому, що читаєте в Інтернеті, особливо не з тієї відповіді StackOverflow.
Паскаль Куок

1
@PascalCuoq Це руйнує віру кількох ТАК віруючих, як я. Куди йти зараз?
0десяткове0

2

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


2

Що стосується невизначеної поведінки, часто важко відокремити формальні аспекти від практичних. Це визначення невизначеної поведінки у стандарті 1989 року (у мене немає більш пізньої версії, але я не очікую, що це суттєво змінилося):

1 невизначена поведінка
  поведінки, при використанні непереносимої або помилкової конструкції програми або
  помилкові дані, щодо яких цей міжнародний стандарт не вимагає жодних вимог
2 ПРИМІТКА Можлива невизначена поведінка коливається від повного ігнорування ситуації
  з непередбачуваними результатами, до поведінки під час перекладу або виконання програми
  документованим чином, характерним для навколишнього середовища (з
  видача діагностичного повідомлення), припинення перекладу або
  виконання (з видачею діагностичного повідомлення).

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

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


2

Стандарт каже, що, як я добре пам’ятаю, дозволяється робити що завгодно з моменту порушення правила. Можливо, є якісь особливі події з таким загальним колоритом (але я ніколи не чув і не читав про щось подібне) ... Тож я б сказав: Ні, це не може бути UB, бо поки поведінка чітко визначена, 0 завжди false, тому правило не може бути порушено під час виконання.


0 завжди правда? чи навіть завжди правда? ти якийсь рубіст ?!
Граді Грейді

@Grady PlayerNo, я не був якимось Brain afk. IAM збираюся це виправити, вибачте
dhein

2

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

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

Доповідь про дефекти №109 звертається до подібного питання та говорить:

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


-1

Це залежить від того, як визначено вираз "невизначена поведінка", і чи буде "невизначена поведінка" висловлення таким самим, як "невизначена поведінка" для програми.

Ця програма виглядає як C, тому доцільніший більш глибокий аналіз того, який стандарт C використовується компілятором (як це робили деякі відповіді).

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.