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?


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.


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!

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.


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

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.