Dlaczego „d / = d” nie rzuca wyjątku dzielenia przez zero, gdy d == 0?

81

Nie do końca rozumiem, dlaczego nie otrzymuję wyjątku dzielenia przez zero:

int d = 0;
d /= d;

Spodziewałem się, że otrzymam wyjątek dzielenia przez zero, ale zamiast tego d == 1.

Dlaczego nie d /= dzgłasza wyjątku dzielenia przez zero, kiedy d == 0?

Valerii Boldakov
źródło
25
To niezdefiniowane zachowanie.
LF
51
Nie ma czegoś takiego jak wyjątek dzielenia przez zero.
πάντα ῥεῖ
15
Aby wyjaśnić niektóre komentarze: kiedy widzisz komunikat o „wyjątku dzielenia przez zero”, oznacza to, że system operacyjny informuje Cię, że coś poszło nie tak. To nie jest wyjątek w C ++. W C ++ wyjątki są generowane przez throwinstrukcję. Nic więcej (chyba że jesteś w krainie nieokreślonych zachowań).
Pete Becker
9
W C ++ nie ma czegoś takiego jak „ wyjątek dzielenia przez zero ”.
Algirdas Preidžius
6
@ user11659763 "Dlatego jest to niezdefiniowane zachowanie: całkowicie zależy od celu." - To nie to, co nieokreślone środki zachowania w ogóle ; to, co opisujesz, to zachowanie zdefiniowane w implementacji . Niezdefiniowane zachowanie jest dużo, dużo silniejszym stwierdzeniem.
marcelm

Odpowiedzi:

108

C ++ nie ma wyjątku „Dzielenie przez zero” do przechwycenia. Zachowanie, które obserwujesz, jest wynikiem optymalizacji kompilatora:

  1. Kompilator zakłada, że ​​niezdefiniowane zachowanie nie występuje
  2. Dzielenie przez zero w C ++ jest niezdefiniowanym zachowaniem
  3. Dlatego zakłada się , że kod, który może powodować dzielenie przez zero, nie robi tego.
    • Zakłada się , że kod, który musi spowodować podział na zero, nigdy się nie wydarzy
  4. Dlatego kompilator wnioskuje, że ponieważ nie występuje niezdefiniowane zachowanie, to warunki dla niezdefiniowanego zachowania w tym kodzie ( d == 0) nie mogą wystąpić
  5. Dlatego d / dzawsze musi wynosić 1.

Jednak...

Możemy zmusić kompilator do wywołania „prawdziwego” dzielenia przez zero z niewielkimi zmianami w kodzie.

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

Więc teraz pozostaje pytanie: teraz, kiedy w zasadzie zmusiliśmy kompilator, aby pozwolił na to, co się dzieje? Jest to niezdefiniowane zachowanie - ale teraz uniemożliwiliśmy kompilatorowi optymalizację wokół tego niezdefiniowanego zachowania.

W większości zależy to od środowiska docelowego. To nie wyzwoli wyjątku programowego, ale może (w zależności od docelowego procesora) wyzwolić wyjątek sprzętowy (Integer-Divide-by-zero), którego nie można przechwycić w tradycyjny sposób, można przechwycić wyjątek programowy. Tak jest z pewnością w przypadku procesora x86 i większości innych (ale nie wszystkich!) Architektur.

Istnieją jednak metody radzenia sobie z wyjątkiem sprzętowym (jeśli wystąpi) zamiast po prostu pozwolić na awarię programu: spójrz na ten post, aby znaleźć kilka metod, które mogą mieć zastosowanie: Wyłapywanie wyjątku: dzielenie przez zero . Zauważ, że różnią się one od kompilatora do kompilatora.

Xirema
źródło
25
@Adrian Oba są całkowicie w porządku, ponieważ zachowanie jest niezdefiniowane. Dosłownie wszystko jest w porządku.
Jesper Juhl
9
„Dzielenie przez zero w C ++ jest niezdefiniowanym zachowaniem” -> zauważ, że kompilator nie może dokonać tej optymalizacji dla typów zmiennoprzecinkowych w IEE754. Musi ustawić d na NaN.
Batszeba
6
@RichardHodges kompilator działający w ramach IEEE754 nie może wykonywać optymalizacji pod kątem double: NaN musi zostać utworzony.
Batszeba
2
@formerlyknownas: Nie chodzi o „optymalizację z dala od UB” - nadal jest tak, że „wszystko może się zdarzyć”; po prostu produkcja 1jest czymś całkowicie ważnym. Uzyskanie 14684554 musi wynikać z tego, że kompilator optymalizuje jeszcze bardziej - propaguje d==0warunek początkowy i dlatego może stwierdzić nie tylko „to jest 1 lub UB”, ale w rzeczywistości „to jest UB, kropka”. Dlatego nawet nie zawraca sobie głowy tworzeniem kodu, który ładuje stałą 1.
hmakholm opuścił Monikę
1
Ludzie zawsze sugerują niestabilność, aby zapobiec optymalizacji, ale to, co stanowi ulotny odczyt lub zapis, jest zdefiniowane przez implementację.
philipxy
38

Aby uzupełnić inne odpowiedzi, fakt, że dzielenie przez zero jest niezdefiniowanym zachowaniem, oznacza, że kompilator może zrobić wszystko w przypadkach, w których tak się stanie:

  • Kompilator może to założyć 0 / 0 == 1i odpowiednio zoptymalizować. Wydaje się, że właśnie to tutaj zrobiło.
  • Kompilator mógłby również, gdyby chciał, założyć to 0 / 0 == 42i ustawić dna tę wartość.
  • Kompilator może również zdecydować, że wartość djest nieokreślona, ​​a tym samym pozostawić zmienną niezainicjowaną, tak aby jej wartość była tym, co zostało wcześniej zapisane w przydzielonej jej pamięci. Niektóre z nieoczekiwanych wartości obserwowanych w komentarzach w innych kompilatorach mogą być spowodowane przez te kompilatory, które robią coś takiego.
  • Kompilator może również zdecydować o przerwaniu programu lub zgłoszeniu wyjątku, gdy nastąpi dzielenie przez zero. Ponieważ w przypadku tego programu kompilator może określić, że tak się stanie zawsze , może po prostu wyemitować kod, aby zgłosić wyjątek (lub całkowicie przerwać wykonywanie) i traktować resztę funkcji jako nieosiągalny kod.
  • Zamiast zgłaszać wyjątek, gdy następuje dzielenie przez zero, kompilator może również zatrzymać program i zamiast tego rozpocząć grę w pasjansa. To również wchodzi w zakres „niezdefiniowanego zachowania”.
  • W zasadzie kompilator mógłby nawet wydać kod, który spowodowałby eksplozję komputera za każdym razem, gdy następuje dzielenie przez zero. W standardzie C ++ nie ma nic, co by tego zabraniało. (W przypadku niektórych zastosowań, takich jak kontroler lotu pocisku, może to być nawet uznane za pożądane zabezpieczenie!)
  • Ponadto standard wyraźnie zezwala na „podróż w czasie” niezdefiniowanym zachowaniom , więc kompilator może również wykonać dowolną z powyższych czynności (lub cokolwiek innego), zanim nastąpi dzielenie przez zero. Zasadniczo standard pozwala kompilatorowi dowolnie zmieniać kolejność operacji, o ile obserwowalne zachowanie programu nie ulegnie zmianie - ale nawet ten ostatni wymóg jest wyraźnie uchylony, jeśli wykonanie programu spowodowałoby niezdefiniowane zachowanie. W efekcie całe zachowanie dowolnego wykonania programu, które w pewnym momencie wywołałoby niezdefiniowane zachowanie, jest niezdefiniowane!
  • W konsekwencji powyższego kompilator może po prostu założyć, że niezdefiniowane zachowanie nie występuje , ponieważ jednym z dopuszczalnych zachowań programu, który zachowywałby się w nieokreślony sposób na niektórych danych wejściowych, jest po prostu zachowanie się tak, jakby dane wejściowe były czymś jeszcze . Oznacza to, że nawet jeśli oryginalna wartość dnie była znana w czasie kompilacji, kompilator nadal może założyć, że nigdy nie jest równa zero i odpowiednio zoptymalizować kod. W konkretnym przypadku kodu OP jest to praktycznie nie do odróżnienia od kompilatora, który po prostu tak założył 0 / 0 == 1, ale kompilator może również, na przykład, założyć, że puts()in if (d == 0) puts("About to divide by zero!"); d /= d;nigdy nie zostanie wykonany!
Ilmari Karonen
źródło
29

Zachowanie dzielenia liczby całkowitej przez zero jest niezdefiniowane w standardzie C ++. Jest nie wymaga się wyjątek.

(Dzielenie zmiennoprzecinkowe przez zero jest również niezdefiniowane, ale definiuje to IEEE754).

Twój kompilator optymalizuje d /= dsię, d = 1co jest rozsądnym wyborem. Dozwolone jest dokonanie tej optymalizacji, ponieważ można założyć, że w kodzie nie ma niezdefiniowanego zachowania - to dnie może wynosić zero.

Batszeba
źródło
3
Ważne jest, aby było bardzo jasne, że może się zdarzyć coś innego, IOW nie można polegać na tym zachowaniu.
hyde
2
Kiedy mówisz, że kompilator powinien założyć, że „to dnie może być zero”, czy też zakładasz, że kompilator nie widzi linii: int d = 0;?? :)
Adrian Mole
6
Kompilator to widzi, ale prawdopodobnie nie przejmuje się tym. Dodatkowa złożoność kodu wymagana w już szalonym, złożonym kompilatorze w przypadku krawędzi takich jak ten, prawdopodobnie nie jest tego warta.
user4581301
1
@ user4581301 Wzięcie obu razem pozwala wykryć zatrutą gałąź, umożliwiając przycięcie znacznie większej ilości kodu. Więc byłoby to przydatne.
Deduplikator
3
Więc jeśli napisałeś "int d = 0; if (d == 0) printf (" d = zero \ n "); d / = d;", ​​kompilator może również usunąć printf.
gnasher729
0

Zwróć uwagę, że możesz sprawić, by kod wygenerował wyjątek C ++ w tym (i innych przypadkach) przy użyciu bezpiecznych liczb zwiększonych. https://github.com/boostorg/safe_numerics

Robert Ramey
źródło