Ostrzeżenie C ++: dzielenie podwójnego przez zero

98

Przypadek 1:

#include <iostream>

int main()
{
    double d = 15.50;
    std::cout<<(d/0.0)<<std::endl;
}

Kompiluje się bez żadnych ostrzeżeń i wydruków inf. OK, C ++ radzi sobie z dzieleniem przez zero ( zobacz na żywo ).

Ale,

Przypadek 2:

#include <iostream>

int main()
{
    double d = 15.50;
    std::cout<<(d/0)<<std::endl;
}

Kompilator daje następujące ostrzeżenie ( zobacz na żywo ):

warning: division by zero [-Wdiv-by-zero]
     std::cout<<(d/0)<<std::endl;

Dlaczego kompilator daje ostrzeżenie w drugim przypadku?

Jest 0 != 0.0?

Edytować:

#include <iostream>

int main()
{
    if(0 == 0.0)
        std::cout<<"Same"<<std::endl;
    else
        std::cout<<"Not same"<<std::endl;
}

wynik:

Same
Jayesh
źródło
9
Zakładam, że w drugim przypadku przyjmuje zero jako liczbę całkowitą i odrzuca ostrzeżenie, nawet jeśli obliczenia zostaną później wykonane przy użyciu podwójnego (co moim zdaniem powinno być zachowaniem, gdy d jest podwójne).
Qubit
10
Tak naprawdę to kwestia QoI. Ani ostrzeżenie, ani brak ostrzeżenia nie jest czymś narzuconym przez sam standard C ++. Czy używasz GCC?
StoryTeller - Unslander Monica
5
@StoryTeller Co to jest QoI? en.wikipedia.org/wiki/QoI ?
user202729
5
Odnosząc się do ostatniego pytania, „czy 0 to to samo co 0,0?” Odpowiedź brzmi: wartości są takie same, ale jak się przekonałeś, nie oznacza to, że są identyczne. Różne rodzaje! Tak jak „A” nie jest identyczne z 65.
Pan Lister

Odpowiedzi:

108

Dzielenie zmiennoprzecinkowe przez zero jest dobrze zdefiniowane przez IEEE i daje nieskończoność (dodatnią lub ujemną w zależności od wartości licznika (lub NaNdla ± 0) ).

W przypadku liczb całkowitych nie ma sposobu, aby przedstawić nieskończoność, a język definiuje operację tak, aby miała niezdefiniowane zachowanie, więc kompilator z łatwością próbuje odciągnąć Cię od tej ścieżki.

Jednak w tym przypadku, ponieważ licznikiem jest a double, dzielnik ( 0) również powinien zostać podwyższony do podwójnej wartości i nie ma powodu, aby dawać ostrzeżenie, nie dając ostrzeżenia, 0.0więc myślę, że jest to błąd kompilatora.

Motti
źródło
8
Oba są jednak działami zmiennoprzecinkowymi. W d/0, 0jest konwertowane na typ d.
43
Zauważ, że C ++ nie jest wymagane do korzystania z IEEE 754 (chociaż nigdy nie widziałem kompilatora używającego innego standardu) ..
Yksisarvinen
1
@hvd, dobra uwaga, w takim razie wygląda to na błąd kompilatora
Motti
14
Zgodziłbym się, że powinien albo ostrzegać w obu przypadkach, albo nie ostrzegać w obu przypadkach (w zależności od tego, jak kompilator obsługuje zmienne dzielenie przez zero)
MM
8
Całkiem pewny, że dzielenie zmiennoprzecinkowe przez zero to również UB - po prostu GCC implementuje to zgodnie z IEEE 754. Jednak nie muszą tego robić.
Martin Bonner wspiera Monikę
42

W standardowym C ++ oba przypadki są niezdefiniowanym zachowaniem . Wszystko może się zdarzyć, łącznie z formatowaniem dysku twardego. Nie należy oczekiwać ani polegać na „return inf. Ok” ani na jakimkolwiek innym zachowaniu.

Kompilator najwyraźniej decyduje się na ostrzeżenie w jednym przypadku, a nie w drugim, ale to nie znaczy, że jeden kod jest w porządku, a drugi nie. To tylko dziwactwo generowanych przez kompilator ostrzeżeń.

Ze standardu C ++ 17 [wyr.mul] / 4:

/Operator binarny daje iloraz, a %operator binarny zwraca resztę z dzielenia pierwszego wyrażenia przez drugie. Jeśli drugi argument operacji /lub %wynosi zero, zachowanie jest niezdefiniowane.

MM
źródło
21
Nieprawda, w arytmetyce zmiennoprzecinkowej dzielenie przez zero jest dobrze zdefiniowane.
Motti
9
@Motti - Jeśli ktoś ograniczy się do samego standardu C ++, nie ma takich gwarancji. Szczerze mówiąc, zakres tego pytania nie jest dobrze określony.
StoryTeller - Unslander Monica
9
@StoryTeller Jestem prawie pewien (chociaż nie patrzyłem na sam standardowy dokument), że jeśli std::numeric_limits<T>::is_iec559tak true, to dzielenie przez zero Tnie jest UB (i na większości platform jest truedla doublei float, chociaż być przenośnym, trzeba to wyraźnie zaznaczyć za pomocą iflub if constexpr).
Daniel H,
6
@DanielH - „Rozsądne” jest właściwie dość subiektywne. Gdyby to pytanie zostało oznaczone jako język-prawnik , powstałby cały inny (znacznie mniejszy) zestaw rozsądnych założeń.
StoryTeller - Unslander Monica
5
@MM Pamiętaj, że to dokładnie to, co powiedziałeś, ale nic więcej: undefined nie oznacza, że ​​nie są narzucane żadne wymagania, to znaczy, że norma nie narzuca żadnych wymagań . Twierdzę, że w tym przypadku fakt, że implementacja definiuje is_iec559jako trueoznacza, że implementacja dokumentuje zachowanie, które norma nie definiuje . Po prostu jest to jeden przypadek, w którym dokumentację implementacji można odczytać programowo. Nawet nie jedyny: to samo dotyczy is_moduloliczb całkowitych ze znakiem.
12

Moim najlepszym przypuszczeniem, aby odpowiedzieć na to konkretne pytanie, byłoby to, że kompilator emituje ostrzeżenie przed wykonaniem konwersji intdo double.

Tak więc kroki wyglądałyby tak:

  1. Wyrażenie analizy
  2. Operator arytmetyczny /(T, T2) , w którym T=double, T2=int.
  3. Sprawdź, czy to std::is_integral<T2>::valuejest truei b == 0- to wyzwala ostrzeżenie.
  4. Emituj ostrzeżenie
  5. Wykonaj niejawną konwersję T2nadouble
  6. Wykonaj dobrze zdefiniowany podział (ponieważ kompilator zdecydował się na użycie IEEE 754).

Jest to oczywiście spekulacja i opiera się na specyfikacjach zdefiniowanych przez kompilator. Ze standardowego punktu widzenia mamy do czynienia z możliwymi niezdefiniowanymi zachowaniami.


Należy pamiętać, że jest to oczekiwane zachowanie zgodnie z dokumentacją GCC
(przy okazji wydaje się, że ta flaga nie może być używana jawnie w GCC 8.1)

-Wdiv-by-zero
Ostrzega o dzieleniu liczby całkowitej w czasie kompilacji przez zero. To jest ustawienie domyślne. Aby zablokować komunikaty ostrzegawcze, użyj -Wno-div-by-zero. Nie ostrzega się przed dzieleniem zmiennoprzecinkowym przez zero, ponieważ może to być uzasadniony sposób uzyskania nieskończoności i NaN.

Yksisarvinen
źródło
2
Nie tak działają kompilatory C ++. Kompilator musi przeprowadzić rozpoznanie przeciążenia, /aby wiedzieć, że jest dzielony. Gdyby lewa strona była Fooprzedmiotem i byłby operator/(Foo, int), to może nawet nie być podziałem. Kompilator zna podział tylko wtedy, gdy wybrał built-in / (double, double)niejawną konwersję prawej strony. Ale to znaczy, że NIE dzieli przez int(0), tylko dzieli double(0).
MSalters
@MSalters Zobacz to. Moja wiedza na temat C ++ jest ograniczona, ale zgodnie z odniesieniem operator /(double, int)jest z pewnością akceptowalna. Następnie mówi, że konwersja jest wykonywana przed jakąkolwiek inną akcją, ale GCC może wcisnąć w szybkie sprawdzenie, czy T2jest to typ całkowity b == 0i wyemitować ostrzeżenie, jeśli tak. Nie jestem pewien, czy jest to w pełni zgodne ze standardami, ale kompilatory mają pełną swobodę w definiowaniu ostrzeżeń i kiedy powinny być uruchamiane.
Yksisarvinen
2
Mówimy tutaj o wbudowanym operatorze. To zabawne. W rzeczywistości nie jest to funkcja, więc nie możesz wziąć jej adresu. Dlatego nie możesz określić, czy operator/(double,int)naprawdę istnieje. Kompilator może na przykład zdecydować się na optymalizację a/bpod kątem stałej b, zastępując ją a * (1/b). Oczywiście oznacza to, że nie dzwonisz już operator/(double,double)w czasie wykonywania, ale szybciej operator*(double,double). Ale teraz to optymalizator się 1/0operator*
potyka
@MSalters Generalnie dzielenie zmiennoprzecinkowe nie może być zastąpione mnożeniem, prawdopodobnie poza wyjątkowymi przypadkami, takimi jak 2.
user202729
2
@ user202729: GCC robi to nawet dla dzielenia liczb całkowitych . Pozwól temu na chwilę zapaść. GCC zastępuje dzielenie liczb całkowitych mnożeniem liczb całkowitych. Tak, to możliwe, ponieważ GCC wie, że działa na pierścieniu (liczby modulo 2 ^ N)
MSalters
9

W tej odpowiedzi nie będę wchodził w porażkę UB / nie UB.

Chcę tylko na to zwrócić uwagę 0i jestem 0.0 inny, mimo że 0 == 0.0oceniam jako prawda. 0jest intliterałem i 0.0jest doubleliterałem.

Jednak w tym przypadku wynik końcowy jest taki sam: d/0jest dzieleniem zmiennoprzecinkowym, ponieważ djest podwójny, a więc 0jest niejawnie konwertowany na podwójny.

bolov
źródło
5
Nie rozumiem, jak to ma znaczenie, biorąc pod uwagę, że zwykłe konwersje arytmetyczne określają, że dzielenie a doubleprzez intśrodek, na który intjest konwertowane double , i jest określone w standardzie, który 0konwertuje na 0.0 (konw. Fpint / 2)
MM
@MM OP chce wiedzieć, czy 0to to samo co0.0
bolov
2
Pytanie brzmi „jest 0 != 0.0?”. OP nigdy nie pyta, czy są „takie same”. Wydaje mi się również, że celem pytania jest to, czy d/0można zachowywać się inaczej niżd/0.0
MM
2
@MM - OP zapytał . Tak naprawdę nie pokazują przyzwoitej netykiety SO przy tych zmianach stałych.
StoryTeller - Unslander Monica
7

Uważam, że foo/0i foo/0.0to nie to samo. Mianowicie wynikowy efekt pierwszego (dzielenie całkowite lub zmiennoprzecinkowe) jest silnie zależny od typu foo, podczas gdy to samo nie dotyczy drugiego (zawsze będzie to dzielenie zmiennoprzecinkowe).

Nie ma znaczenia, czy którykolwiek z nich jest UB. Cytując standard:

Dopuszczalne niezdefiniowane zachowanie waha się od całkowitego zignorowania sytuacji z nieprzewidywalnymi skutkami, przez zachowanie podczas tłumaczenia lub wykonywania programu w udokumentowany sposób charakterystyczny dla środowiska (z lub bez wydania komunikatu diagnostycznego) , po przerwanie tłumaczenia lub wykonania (z wydaniem komunikatu diagnostycznego).

(Podkreślenie moje)

Rozważ ostrzeżenie „ sugeruj nawiasy wokół przypisania używanego jako wartość prawdy ”: Sposobem na poinformowanie kompilatora, że naprawdę chcesz użyć wyniku przypisania, jest wyraźne umieszczenie przypisania i dodanie nawiasów wokół przypisania. Wynikowa instrukcja ma ten sam efekt, ale informuje kompilator, że wiesz, co robisz. To samo można powiedzieć o foo/0.0: skoro wyraźnie mówisz kompilatorowi „To jest dzielenie zmiennoprzecinkowe” używając 0.0zamiast 0, kompilator ufa ci i nie wyśle ​​ostrzeżenia.

Cássio Renan
źródło
1
Oba muszą przejść zwykłe konwersje arytmetyczne, aby uzyskać wspólny typ, który pozostawi podział zmiennoprzecinkowy w obu przypadkach.
Shafik Yaghmour
@ShafikYaghmour W odpowiedzi przegapiłeś punkt. Zauważ, że nigdy nie wspomniałem, jaki jest typ foo. To jest zamierzone. Twoje stwierdzenie jest prawdziwe tylko w przypadku footypu zmiennoprzecinkowego.
Cássio Renan
Nie zrobiłem tego, kompilator ma informacje o typie i rozumie konwersje, być może analizator statyczny oparty na tekście może zostać złapany przez takie rzeczy, ale kompilator nie powinien.
Shafik Yaghmour
Chodzi mi o to, że tak, kompilator zna zwykłe konwersje arytmetyczne, ale decyduje się nie wystawiać ostrzeżenia, gdy programista jest jawny. Chodzi o to, że prawdopodobnie nie jest to błąd, ale zamiast tego jest to zamierzone zachowanie.
Cássio Renan
Wtedy wskazana przeze mnie dokumentacja jest niepoprawna, ponieważ oba przypadki to dzielenie zmiennoprzecinkowe. Więc albo dokumentacja jest błędna, albo diagnostyka zawiera błąd.
Shafik Yaghmour
4

Wygląda to na błąd w gcc, dokumentacja -Wno-div-by-zero jasno mówi :

Nie ostrzegaj o dzieleniu liczby całkowitej w czasie kompilacji przez zero. Nie ostrzega się przed dzieleniem zmiennoprzecinkowym przez zero, ponieważ może to być uzasadniony sposób uzyskania nieskończoności i NaN.

a po zwykłych konwersjach arytmetycznych uwzględnionych w [wyraż.arith.conv] oba operandy będą podwójne :

Wiele operatorów binarnych, które oczekują operandów typu arytmetycznego lub wyliczeniowego, powoduje konwersje i zwraca typy wyników w podobny sposób. Celem jest uzyskanie wspólnego typu, który jest również typem wyniku. Ten wzorzec nazywa się zwykłymi konwersjami arytmetycznymi, które są zdefiniowane w następujący sposób:

...

- W przeciwnym razie, jeśli jeden z argumentów jest podwójny, drugi zostanie przekonwertowany na podwójny.

i [wyr.mul] :

Operandy * i / powinny mieć arytmetyczny lub nieograniczony typ wyliczenia; operandy% mają typ wyliczenia całkowitego lub nieograniczonego. Zwykłe konwersje arytmetyczne są wykonywane na operandach i określają typ wyniku.

W odniesieniu do tego, czy dzielenie zmiennoprzecinkowe przez zero jest niezdefiniowanym zachowaniem i jak radzą sobie z tym różne implementacje, wydaje mi się, że tutaj odpowiedź . TL; DR; Wygląda na to, że gcc jest zgodne z załącznikiem F wrt do dzielenia zmiennoprzecinkowego przez zero, więc wartość undefined nie odgrywa tutaj roli. W przypadku brzęku odpowiedź byłaby inna.

Shafik Yaghmour
źródło
2

Dzielenie zmiennoprzecinkowe przez zero zachowuje się inaczej niż dzielenie liczby całkowitej przez zero.

W IEEE zmiennoprzecinkowych standardowe rozróżnia + inf i -Inf, natomiast całkowite nie można przechowywać w nieskończoność. Wynik dzielenia liczby całkowitej przez zero jest niezdefiniowanym zachowaniem. Dzielenie zmiennoprzecinkowe przez zero jest zdefiniowane przez standard zmiennoprzecinkowy i daje w wyniku + inf lub -inf.

Rizwan
źródło
2
To prawda, ale nie jest jasne, jakie ma to znaczenie dla pytania, ponieważ dzielenie zmiennoprzecinkowe jest wykonywane w obu przypadkach . W kodzie OP nie ma dzielenia liczb całkowitych.
Konrad Rudolph