Der Feind aller Arithmetik
Freitag, 22. Oktober 2010 | Autor: Nico
Neulich bin ich auf einen Fehler gestoßen, der erstmal so schlimm gar nicht aussieht:
interpolate(double, const P&, const P&) [with P = FVector]: Assertion `t >= 0.0 && t <= 1.0' failed.
Die Fehlermeldung sagt uns, dass eine Assertion fehlgeschlagen ist. Offenbar ist der Parameter t
nicht zwischen 0 und 1. Aber das ist noch längst nicht alles..
Beispiel
Da das Programm in dem der Fehler ursprünglich auftrat, sehr komplex ist, illustriere ich den Fehler hier lieber an einem kleinen (zwecklosen) Beispielprogramm:
#include <iostream> #include <cassert> int main() { double a = 12/24; double b = -31/38; double t = a/b; assert( t>=0.0 and t=1.0 ); }
Aus Gründen, die wir erst noch herausfinden müssen, schlägt die Assertion1 an:
Assertion failed: t>=0.0 and t=1.0, file nan.cpp, line 9
Um den Fehler zu vermeiden, habe ich erstmal den stupidesten Bugfix verwendet, den man sich denken kann. Ich fange den Fehler einfach schon vorher ab, indem ich in Zeile 8 eine if
-Abfrage einbaue, die ggf. eine Fehlermeldung ausgibt und das Programm ordentlich beendet.
#include <iostream> #include <cassert> int main() { double a = 12/24; double b = -31/38; double t = a/b; if( t0.0 or t>1.0) { std::cout "t muss im Bereich [0,1] liegen!" std::endl; return 0; } assert( t>=0.0 and t=1.0 ); }
doch…
Assertion failed: t>=0.0 and t=1.0, file nan.cpp, line 13
Seltsam - unser if
überprüft doch exakt diese Bedingung. Bis zur Assertion sollte das Programm eigentlich gar nicht kommen!
Die Ursache
Lassen wir uns doch t
einfach mal ausgeben und schreiben nach Zeile 7:
std::cout t std::endl;
Nun bekommen wir:
nan
Assertion failed: t>=0.0 and t=1.0, file nan.cpp, line 14
t ist nan! NaN - Not a Number bedeutet, dass die Variable einen Wert enthält, der keinen Sinn ergibt. Alle Berechnungen, die NaN enthalten, ergeben selbst wieder NaN.
Ich will’s jetzt genau wissen und modifiziere meinen Code so, dass er praktisch alles ausgibt:
#include <iostream> #include <cassert> int main() { double a = 12/24; double b = -31/38; double t = a/b; std::cout "a: " a std::endl; std::cout "b: " b std::endl; std::cout "t: " t std::endl; std::cout "t>0.0: " (t>0.0) std::endl; std::cout "t=0.0: " (t>0.0) std::endl; std::cout "t1.0: " (t1.0) std::endl; std::cout "t>=1.0: " (t1.0) std::endl; std::cout "t>0.0 and t1.0: " (t>0.0 and t1.0) std::endl; if( t0.0 or t>1.0) { std::cout "t muss im Bereich [0,1] liegen!" std::endl; return 0; } assert( t>=0.0 and t=1.0 ); }
produziert
a: 0
b: 0
t: nan
t>0.0: 0
t=0.0: 0
t1.0: 0
t>=1.0: 0
t>0.0 and t1.0: 0
Assertion failed: t>0.0 and t1.0, file nan.cpp, line 21
Aha! Der Vergleich mit NaN ergibt einfach immer false2
! Auch unsere Prüfung in Zeile 16, ob t
außerhalb des gewünschten Bereichs ist, ergibt also stets false
.
Auch wird nun klar, warum unser t
den Wert NaN bekommen hat. a
ist 0, b
ist 0. t
ist somit 0/0, was keine sinnvolle Zahl ist. Es gibt noch mehr solcher seltsamen Zahlen. Eine Division einer Zahl durch Null ergibt beispielsweise inft - Unendlich.3
Der Fix
Der korrekte Bugfix hier ist, die Division durch Null abzufangen. Was man dann sinnvollerweise tut, hängt natürlich von der konkreten Situation ab. Für das kleine Beispiel hier gebe ich nur eine Fehlermeldung aus und beende das Programm4.
#include <iostream> #include <cassert> int main() { double a = 12/24; double b = -31/38; if( b==0) { std::cout "Fehler: Division durch Null!" std::endl; return 0; } double t = a/b; assert( t>0.0 and t1.0 ); }
PS
Warum werden a
und b
überhaupt 0? Ihre Berechnung besteht nur aus Integers, dewegen benutzt C++ hier Integer-Artihmetik und das Ergebnis ist wiederum ein Integer - dadurch sind die wahren Ergebnisse 0.5 und -0.81 quasi abgerundet - also zu 0.
PPS
Man kann auf NaN testen, indem man den Wert mit sich selbst vergleicht. Dieser Vergleich ergibt bei NaN false! Näheres zum Thema gibt es auf der GNU-Website: gnu.org/s/hello/manual/libc/Infinity-and-NaN.html
- Assertions dienen dazu, sich einer bestimmten Tatsache zu versichern (assert=versichern). Man gibt dazu eine Bedingung an, die erfüllt sein soll. Ist sie es nicht, d.h. ist die Bedingung
false
, beendet sich das Programm. Nicht besonders elegant und sollte abseits von Debugging vermieden werden. ↩ - 0 ist gleichbedeutend mit
false
↩ - siehe WP:NaN für weitere Informationen ↩
- Nicht empfohlen für Raumfahrzeuge oder Herzschrittmacher. ↩