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 aussieht:
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­ti­on 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..

Beispiel

Da das Pro­gramm in dem der Feh­ler ursprüng­lich auf­trat, sehr kom­plex ist, illus­trie­re 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­ti­on1 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 fan­ge den Feh­ler ein­fach schon vor­her ab, indem ich in Zei­le 8 eine if-Abfra­ge 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( 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
Selt­sam - unser if über­prüft doch exakt die­se Bedin­gung. Bis zur Asser­ti­on soll­te das Pro­gramm eigent­lich gar nicht kommen!

Die Ursache

Las­sen wir uns doch t ein­fach mal aus­ge­ben und schrei­ben nach Zei­le 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­zie­re 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  "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 );
}

pro­du­ziert
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 Ver­gleich mit NaN ergibt ein­fach immer false2! Auch unse­re Prü­fung in Zei­le 16, ob t außer­halb des gewünsch­ten Bereichs ist, ergibt also stets false.

Auch wird nun klar, war­um unser t den Wert NaN bekom­men hat. a ist 0, b ist 0. t ist somit 0/0, was kei­ne sinn­vol­le Zahl ist. Es gibt noch mehr sol­cher selt­sa­men Zah­len. Eine Divi­si­on einer Zahl durch Null ergibt bei­spiels­wei­se inft - Unend­lich.3

Der Fix

Der kor­rek­te Bug­fix hier ist, die Divi­si­on durch Null abzu­fan­gen. Was man dann sinn­vol­ler­wei­se tut, hängt natür­lich von der kon­kre­ten Situa­ti­on ab. Für das klei­ne Bei­spiel hier gebe ich nur eine Feh­ler­mel­dung aus und been­de 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 t1.0 );
}

PS

War­um wer­den a und b über­haupt 0? Ihre Berech­nung besteht nur aus Inte­gers, dewe­gen benutzt C++ hier Inte­ger-Artih­me­tik und das Ergeb­nis ist wie­der­um ein Inte­ger - dadurch sind die wah­ren Ergeb­nis­se 0.5 und -0.81 qua­si abge­run­det - also zu 0.

PPS

Man kann auf NaN tes­ten, indem man den Wert mit sich selbst ver­gleicht. Die­ser Ver­gleich ergibt bei NaN fal­se! Nähe­res zum The­ma gibt es auf der GNU-Web­site: gnu.org/s/hello/manual/libc/Infinity-and-NaN.html

  1. Asser­ti­ons die­nen dazu, sich einer bestimm­ten Tat­sa­che zu ver­si­chern (assert=versichern). Man gibt dazu eine Bedin­gung an, die erfüllt sein soll. Ist sie es nicht, d.h. ist die Bedin­gung false, been­det sich das Pro­gramm. Nicht beson­ders ele­gant und soll­te abseits von Debug­ging ver­mie­den wer­den.
  2. 0 ist gleich­be­deu­tend mit false
  3. sie­he WP:NaN für wei­te­re Infor­ma­tio­nen
  4. Nicht emp­foh­len für Raum­fahr­zeu­ge oder Herz­schritt­ma­cher.
Tags »

Trackback: