Home

Der Feind aller Arithmetik

Freitag, 22. Oktober 2010 | Autor:

Neu­lich bin ich auf einen Feh­ler gesto­ßen, der erst­mal so schlimm gar nicht aus­sieht:
interpolate(double, const P&, const P&) [with P = FVector]: Assertion `t >= 0.0 && t <= 1.0' failed.
Die Feh­ler­mel­dung sagt uns, dass eine Asser­tion fehl­ge­schla­gen ist. Offen­bar ist der Para­me­ter t nicht zwi­schen 0 und 1. Aber das ist noch längst nicht alles..

Bei­spiel

Da das Pro­gramm in dem der Feh­ler ursprüng­lich auf­trat, sehr kom­plex ist, illus­triere ich den Feh­ler hier lie­ber an einem klei­nen (zweck­lo­sen) 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ün­den, die wir erst noch her­aus­fin­den müs­sen, schlägt die Asser­tion1 an:
Assertion failed: t>=0.0 and t<=1.0, file nan.cpp, line 9

Um den Feh­ler zu ver­mei­den, habe ich erst­mal den stu­pi­des­ten Bug­fix ver­wen­det, den man sich den­ken kann. Ich fange den Feh­ler ein­fach schon vor­her ab, indem ich in Zeile 8 eine if-Abfrage ein­baue, die ggf. eine Feh­ler­mel­dung aus­gibt und das Pro­gramm ordent­lich beendet.

#include <iostream>
#include <cassert>
int main()
{
	double a = 12/24;
	double b = -31/38;
	double t = a/b;
	if( t<0.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
Selt­sam - unser if über­prüft doch exakt diese Bedin­gung. Bis zur Asser­tion sollte das Pro­gramm eigent­lich gar nicht kommen!

Die Ursa­che

Las­sen wir uns doch t ein­fach mal aus­ge­ben und schrei­ben nach Zeile 7:

std::cout << t << std::endl;

Nun bekom­men wir:
nan
Assertion failed: t>=0.0 and t<=1.0, file nan.cpp, line 14

t ist nan! NaN - Not a Num­ber bedeu­tet, dass die Varia­ble einen Wert ent­hält, der kei­nen Sinn ergibt. Alle Berech­nun­gen, die NaN ent­hal­ten, erge­ben selbst wie­der NaN.

Ich will’s jetzt genau wis­sen und modi­fi­ziere mei­nen Code so, dass er prak­tisch 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 << "t<1.0: "<< (t<1.0) << std::endl;
	std::cout << "t>=1.0: "<< (t<1.0) << std::endl;
	std::cout << "t>0.0 and t<1.0: "<< (t>0.0 and t<1.0) << std::endl;
	if( t<0.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 );
}

pro­du­ziert
a: 0
b: 0
t: nan
t>0.0: 0
t<=0.0: 0 t<1.0: 0 t>=1.0: 0
t>0.0 and t<1.0: 0 Assertion failed: t>0.0 and t<1.0, file nan.cpp, line 21

Aha! Der Ver­gleich mit NaN ergibt ein­fach immer false2! Auch unsere Prü­fung in Zeile 16, ob t außer­halb des gewünsch­ten Bereichs ist, ergibt also stets false.

Auch wird nun klar, warum unser t den Wert NaN bekom­men hat. a ist 0, b ist 0. t ist somit 0/0, was keine sinn­volle Zahl ist. Es gibt noch mehr sol­cher selt­sa­men Zah­len. Eine Divi­sion einer Zahl durch Null ergibt bei­spiels­weise inft - Unend­lich.3

Der Fix

Der kor­rekte Bug­fix hier ist, die Divi­sion durch Null abzu­fan­gen. Was man dann sinn­vol­ler­weise tut, hängt natür­lich von der kon­kre­ten Situa­tion ab. Für das kleine Bei­spiel hier gebe ich nur eine Feh­ler­mel­dung aus und beende das Pro­gramm4.

#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 t<1.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

  1. 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.
  2. 0 ist gleichbedeutend mit false
  3. siehe WP:NaN für weitere Informationen
  4. Nicht empfohlen für Raumfahrzeuge oder Herzschrittmacher.

Tags »

Trackback: Trackback-URL |  Feed zum Beitrag: RSS 2.0
Thema: Sezierte C++-Käfer

Diesen Beitrag kommentieren.

Kommentar abgeben