Чому 'd / = d' не видає ділення на нуль винятку, коли d == 0?


81

Я не зовсім розумію, чому я не отримую ділення на нульовий виняток:

int d = 0;
d /= d;

Я очікував отримати ділення на нульовий виняток, але замість цього d == 1 .

Чому не d /= dвикидає ділення на нульовий виняток, коли d == 0?


25
Це невизначена поведінка.
LF

51
Не існує такого поняття, як поділ на нульові винятки.
πάντα ῥεῖ

15
Щоб пояснити деякі коментарі: коли ви бачите повідомлення про "поділ на нульові винятки", це операційна система, яка повідомляє, що щось пішло не так. Це не виняток на C ++. У C ++ винятки створюються throwоператором. Нічого іншого (якщо ви не на землі з невизначеною поведінкою).
Піт Беккер,

9
У C ++ немає такого поняття, як " ділення на нульовий виняток ".
Альгірдас Преіджіус

6
@ user11659763 "Ось чому це невизначена поведінка: вона повністю залежить від цілі." - Це не те , що не визначене означає поведінку на всіх ; те, що ви описуєте, це поведінка, визначена реалізацією . Невизначена поведінка - це набагато сильніше твердження.
marcelm

Відповіді:


108

C ++ не має винятку "Поділ за нулем", який можна зловити. Поведінка, яку ви спостерігаєте, є результатом оптимізації компілятора:

  1. Компілятор припускає, що невизначеної поведінки не відбувається
  2. Ділення на нуль у C ++ - невизначена поведінка
  3. Отже, код, який може спричинити ділення за нулем, вважається таким, що не робить.
    • І, код, який повинен спричинити ділення за нулем, вважається, ніколи не відбудеться
  4. Отже, компілятор висновує, що оскільки невизначеної поведінки не відбувається, то умови невизначеної поведінки в цьому коді ( d == 0) не повинні відбуватися
  5. Тому d / dзавжди повинен дорівнювати 1.

Однак ...

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

volatile int d = 0;
d /= d; //What happens?

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

Здебільшого це залежить від цільового середовища. Це не спричинить виняток програмного забезпечення, але може (залежно від цільового процесора) викликати апаратне виняток (ціле число-поділ-нуль), яке традиційно не можна визначити, як можна визначити виняток програмного забезпечення. Це точно стосується процесора x86 та більшості інших (але не всіх!) Архітектур.

Однак існують методи боротьби з апаратним винятком (якщо він трапляється) замість того, щоб просто дозволити програмі завершити роботу: подивіться на цю публікацію деякі методи, які можуть бути застосовні: Ловіть виняток: розділіть на нуль . Зверніть увагу, що вони різняться залежно від компілятора.


25
@Adrian І те, і інше цілком нормально, оскільки поведінка невизначена. Буквально що завгодно в порядку.
Jesper Juhl

9
"Розподіл за нулем у C ++ є невизначеною поведінкою" -> зауважте, що компілятор не може здійснити цю оптимізацію для типів з плаваючою комою згідно з IEE754. Потрібно встановити d на NaN.
Вірсавія,

6
@RichardHodges компілятору, що працює за стандартом IEEE754, не дозволяється робити оптимізацію для подвійного: NaN повинен бути створений.
Вірсавія,

2
@formerlyknownas: Справа не в "оптимізації UB" - все одно буває, що "все може статися"; просто виробництво 1- це цілком дійсне що завгодно. Отримання 14684554 повинно бути тому, що компілятор оптимізує ще більше - він поширює початкову d==0умову і, отже, може зробити висновок не лише "це або 1, або UB", а насправді "це UB, точка". Тому навіть не заважає створювати код, який завантажує константу 1.
hmakholm залишив Моніку

1
Люди завжди пропонують нестабільність, щоб запобігти оптимізації, але те, що становить нестабільне читання чи запис, визначається реалізацією.
Філіпсія

38

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

  • Компілятор може припустити це 0 / 0 == 1і відповідно оптимізувати. Це фактично те, що, здається, було зроблено тут.
  • Компілятор також міг би, якщо захотів, припустити це 0 / 0 == 42і встановити dце значення.
  • Компілятор також може вирішити, що значення dє невизначеним, і, таким чином, залишити змінну неініціалізованою, так що її значення буде тим, що раніше було записано в виділену для неї пам'ять. Деякі несподівані значення, які спостерігаються для інших компіляторів у коментарях, можуть бути спричинені тими компіляторами, які роблять щось подібне.
  • Компілятор може також вирішити скасувати програму або створити виняток, коли відбувається поділ на нуль. Оскільки для цієї програми компілятор може визначити, що це завжди відбуватиметься, він може просто випустити код, щоб викликати виняток (або повністю відмінити виконання), а решту функції розглядати як недосяжний код.
  • Замість того, щоб викликати виняток, коли відбувається ділення на нуль, компілятор також може зупинити програму та замість цього розпочати гру в пасьянс. Це також потрапляє під парасольку "невизначеної поведінки".
  • В принципі, компілятор може навіть видавати код, який спричиняє вибух комп'ютера щоразу, коли відбувається поділ на нуль. У стандарті С ++ немає нічого, що забороняло б це. (Для певних видів застосувань, таких як контролер польоту ракет, це навіть можна вважати бажаною функцією безпеки!)
  • Крім того, стандарт явно дозволяє невизначеній поведінці "подорожувати у часі" , так що компілятор може також виконувати будь-що з вищезазначеного (або що-небудь інше) до поділу на нуль. По суті, стандарт дозволяє компілятору вільно перевпорядковувати операції, доки спостережувана поведінка програми не змінюється - але навіть від цієї останньої вимоги явно відмовляються, якщо виконання програми призведе до невизначеної поведінки. Отже, по суті, вся поведінка будь-якого виконання програми, яка в певний момент спричинить невизначену поведінку, є невизначеною!
  • Як наслідок вищесказаного, компілятор може також просто припустити, що невизначеної поведінки не відбувається , оскільки одна допустима поведінка програми, яка поводиться невизначено на деяких входах, полягає в тому, що вона просто поводиться так, ніби вхід був чимось ще . Тобто, навіть якщо початкове значення параметра dне було відоме під час компіляції, компілятор все одно міг припустити, що воно ніколи не дорівнює нулю, і оптимізувати код відповідно. У приватному випадку коду OP це практично 0 / 0 == 1неможливо відрізнити від компілятора, просто припускаючи це , але компілятор також може, наприклад, припустити, що puts()in if (d == 0) puts("About to divide by zero!"); d /= d;ніколи не виконується!

29

Поведінка цілочисельного ділення на нуль не визначена стандартом С ++. Це не так обов'язково кидати виняток.

(Ділення з плаваючою точкою на нуль також не визначено, але IEEE754 це визначає.)

Ваш компілятор оптимізує d /= d, ефективно, d = 1що є розумним вибором. Дозволено робити цю оптимізацію, оскільки дозволяється припускати, що у вашому коді немає невизначеної поведінки - тобто це dне може бути нулем.


3
Важливо бути надто чітким, що також може статися щось інше, IOW на цю поведінку не можна покладатися.
Хайд

2
Коли ви говорите, що для компілятора розумно припустити, "що dне може бути нульовим", ви також вважаєте, що компілятор не бачить рядка: int d = 0;?? :)
Адріан

6
Компілятор це бачить, але, мабуть, все одно. Додаткова складність коду, необхідна в і без того шаленому складному компіляторі для такого передового випадку, мабуть, не варта.
user4581301

1
@ user4581301 Взяття обох разом дозволяє виявити отруєну гілку, що дозволяє обрізати набагато більше коду. Тож було б корисно.
Deduplicator

3
Отже, якщо ви написали "int d = 0; if (d == 0) printf (" d = нуль \ n "); d / = d;" компілятор може також видалити printf.
gnasher729

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