Warum löst 'd / = d' keine Ausnahme für die Division durch Null aus, wenn d == 0 ist?

81

Ich verstehe nicht ganz, warum ich keine Ausnahme durch Null bekomme:

int d = 0;
d /= d;

Ich erwartete aber stattdessen eine Division durch Null-Ausnahme d == 1.

Warum wird keine d /= dAusnahme durch Division durch Null ausgelöst, wenn d == 0?

Valerii Boldakov
quelle
25
Das ist undefiniertes Verhalten.
LF
51
Es gibt keine Ausnahme durch Division durch Null.
πάντα ῥεῖ
15
Um einige der Kommentare zu verdeutlichen: Wenn Sie eine Meldung über eine "Division durch Null-Ausnahme" sehen, teilt Ihnen das Betriebssystem mit, dass ein Fehler aufgetreten ist. Es ist keine C ++ - Ausnahme. In C ++ werden Ausnahmen durch eine throwAnweisung ausgelöst . Nichts anderes (es sei denn, Sie befinden sich in einem Land mit undefiniertem Verhalten).
Pete Becker
9
In C ++ gibt es keine " Division durch Null-Ausnahme ".
Algirdas Preidžius
6
@ user11659763 "Deshalb ist es ein undefiniertes Verhalten: Es hängt vollständig vom Ziel ab." - Das ist nicht das, was Verhalten bedeutet nicht definiert überhaupt ; Was Sie beschreiben, ist implementierungsdefiniertes Verhalten . Undefiniertes Verhalten ist eine viel, viel stärkere Aussage.
Marcelm

Antworten:

108

C ++ hat keine "Division by Zero" -Ausnahme zu fangen. Das beobachtete Verhalten ist das Ergebnis von Compiler-Optimierungen:

  1. Der Compiler geht davon aus, dass undefiniertes Verhalten nicht auftritt
  2. Die Division durch Null in C ++ ist ein undefiniertes Verhalten
  3. Daher wird angenommen, dass Code, der eine Division durch Null verursachen kann , dies nicht tut.
    • Und Code, der eine Division durch Null verursachen muss, wird vermutlich nie vorkommen
  4. Daher schließt der Compiler, dass die Bedingungen für undefiniertes Verhalten in diesem Code ( d == 0) nicht erfüllt sein dürfen , da undefiniertes Verhalten nicht auftritt
  5. Daher d / dmuss immer gleich 1 sein.

Jedoch...

Wir können den Compiler zwingen, eine "echte" Division durch Null mit einer geringfügigen Änderung Ihres Codes auszulösen.

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

Nun bleibt die Frage: Nun, da wir den Compiler grundsätzlich gezwungen haben, dies zuzulassen, was passiert dann? Es ist undefiniertes Verhalten - aber wir haben jetzt verhindert, dass der Compiler dieses undefinierte Verhalten optimiert.

Meistens hängt es von der Zielumgebung ab. Dies löst keine Software-Ausnahme aus, kann jedoch (abhängig von der Ziel-CPU) eine Hardware-Ausnahme (eine Ganzzahl-Division durch Null) auslösen, die nicht auf herkömmliche Weise abgefangen werden kann, wenn eine Software-Ausnahme abgefangen werden kann. Dies ist definitiv der Fall für eine x86-CPU und die meisten anderen (aber nicht alle!) Architekturen.

Es gibt jedoch Methoden, um mit der Hardware-Ausnahme umzugehen (falls sie auftritt), anstatt nur das Programm abstürzen zu lassen: In diesem Beitrag finden Sie einige Methoden, die möglicherweise anwendbar sind: Ausnahme abfangen: durch Null teilen . Beachten Sie, dass sie von Compiler zu Compiler unterschiedlich sind.

Xirema
quelle
25
@Adrian Beide sind vollkommen in Ordnung, da das Verhalten undefiniert ist. Im wahrsten Sinne des Wortes ist alles in Ordnung.
Jesper Juhl
9
"Division durch Null in C ++ ist undefiniertes Verhalten" -> Beachten Sie, dass der Compiler diese Optimierung für Gleitkommatypen unter IEE754 nicht durchführen kann. Es muss d auf NaN setzen.
Bathseba
6
@RichardHodges Ein Compiler, der unter IEEE754 arbeitet, darf die Optimierung für ein Double nicht vornehmen: NaN muss erzeugt werden.
Bathseba
2
@formerlyknownas: Es geht nicht darum, "die UB zu optimieren" - es ist immer noch so, dass "alles passieren kann"; Es ist nur so, dass das Produzieren 1eine vollkommen gültige Sache ist. Das Erhalten von 14684554 muss daran liegen, dass der Compiler noch weiter optimiert - er verbreitet die Anfangsbedingung d==0und kann daher nicht nur auf "dies ist entweder 1 oder UB" schließen, sondern tatsächlich auf "dies ist UB, Punkt". Daher macht es sich nicht einmal die Mühe, Code zu erzeugen, der die Konstante lädt 1.
Hmakholm verließ Monica
1
Die Leute schlagen immer flüchtig vor, um eine Optimierung zu verhindern, aber was ein flüchtiges Lesen oder Schreiben ausmacht, ist implementierungsdefiniert.
Philipxy
38

Um die anderen Antworten zu ergänzen, bedeutet die Tatsache, dass die Division durch Null ein undefiniertes Verhalten ist, dass der Compiler in den Fällen, in denen dies passieren würde, alles tun kann :

  • Der Compiler kann dies annehmen 0 / 0 == 1und entsprechend optimieren. Genau das scheint es hier getan zu haben.
  • Der Compiler könnte dies auch annehmen 0 / 0 == 42und dauf diesen Wert setzen.
  • Der Compiler könnte auch entscheiden, dass der Wert von dunbestimmt ist, und so die Variable nicht initialisieren, so dass ihr Wert der Wert ist, der zuvor in den ihm zugewiesenen Speicher geschrieben wurde. Einige der unerwarteten Werte, die bei anderen Compilern in den Kommentaren beobachtet wurden, können durch diese Compiler verursacht werden, die so etwas tun.
  • Der Compiler kann auch entscheiden, das Programm abzubrechen oder eine Ausnahme auszulösen, wenn eine Division durch Null auftritt. Da der Compiler für dieses Programm bestimmen kann, dass dies immer der Fall ist, kann er einfach den Code ausgeben, um die Ausnahme auszulösen (oder die Ausführung vollständig abzubrechen) und den Rest der Funktion als nicht erreichbaren Code zu behandeln.
  • Anstatt eine Ausnahme auszulösen, wenn eine Division durch Null auftritt, kann der Compiler auch das Programm stoppen und stattdessen ein Solitaire-Spiel starten. Das fällt auch unter das Dach des "undefinierten Verhaltens".
  • Im Prinzip könnte der Compiler sogar Code ausgeben, der dazu führt, dass der Computer explodiert, wenn eine Division durch Null auftritt. Der C ++ - Standard enthält nichts, was dies verbieten würde. (Für bestimmte Arten von Anwendungen, wie z. B. einen Raketenflugregler, kann dies sogar als wünschenswertes Sicherheitsmerkmal angesehen werden!)
  • Darüber hinaus erlaubt der Standard explizit, dass undefiniertes Verhalten "Zeitreisen" durchführt , so dass der Compiler auch eines der oben genannten Dinge (oder irgendetwas anderes) tun kann, bevor die Division durch Null erfolgt. Grundsätzlich erlaubt der Standard dem Compiler, Operationen frei neu zu ordnen, solange das beobachtbare Verhalten des Programms nicht geändert wird - aber selbst auf diese letzte Anforderung wird ausdrücklich verzichtet, wenn die Ausführung des Programms zu einem undefinierten Verhalten führen würde. Tatsächlich ist das gesamte Verhalten einer Programmausführung, die irgendwann ein undefiniertes Verhalten auslösen würde, undefiniert!
  • Infolgedessen kann der Compiler auch einfach davon ausgehen, dass kein undefiniertes Verhalten auftritt , da ein zulässiges Verhalten für ein Programm, das sich bei einigen Eingaben undefiniert verhält, darin besteht, dass es sich einfach so verhält, als ob die Eingabe etwas gewesen wäre sonst . Das heißt, selbst wenn der ursprüngliche Wert von dzum Zeitpunkt der Kompilierung nicht bekannt war, könnte der Compiler dennoch annehmen, dass er niemals Null ist, und den Code entsprechend optimieren. Im speziellen Fall des OP-Codes ist dies praktisch nicht von dem Compiler zu unterscheiden, der dies nur annimmt 0 / 0 == 1, aber der Compiler könnte beispielsweise auch annehmen, dass das puts()In if (d == 0) puts("About to divide by zero!"); d /= d;niemals ausgeführt wird!
Ilmari Karonen
quelle
29

Das Verhalten der Ganzzahldivision durch Null ist vom C ++ - Standard nicht definiert. Es ist nicht erforderlich, eine Ausnahme auszulösen.

(Die Gleitkommadivision durch Null ist ebenfalls undefiniert, wird jedoch von IEEE754 definiert.)

Ihr Compiler optimiert d /= deffektiv, d = 1was eine vernünftige Wahl ist. Diese Optimierung ist zulässig, da davon ausgegangen werden darf, dass Ihr Code kein undefiniertes Verhalten enthält - das dkann unmöglich Null sein.

Bathseba
quelle
3
Es ist wichtig, besonders klar zu sein, dass auch etwas anderes passieren kann. IOW kann sich nicht auf dieses Verhalten verlassen.
Hyde
2
Wenn Sie sagen, dass es für den Compiler vernünftig ist anzunehmen, dass "das dunmöglich Null sein kann", nehmen Sie auch an, dass der Compiler die Zeile nicht sieht: int d = 0;?? :)
Adrian Mole
6
Compiler sieht es, aber es ist ihm wahrscheinlich egal. Die zusätzliche Codekomplexität, die der bereits verrückte komplexe Compiler für einen solchen Edge-Fall benötigt, ist es wahrscheinlich nicht wert.
user4581301
1
@ user4581301 Wenn beide zusammen genommen werden, kann ein vergifteter Zweig erkannt werden, wodurch viel mehr Code beschnitten werden kann. Es wäre also nützlich.
Deduplikator
3
Wenn Sie also "int d = 0; if (d == 0) printf (" d = zero \ n "); d / = d;" geschrieben haben, kann der Compiler auch printf entfernen.
Gnasher729
0

Beachten Sie, dass Ihr Code in diesem (und anderen Fällen) eine C ++ - Ausnahme generieren kann, indem Sie Boost-sichere Zahlen verwenden. https://github.com/boostorg/safe_numerics

Robert Ramey
quelle