ZFX
ZFX Neu
Home
Community
Neueste Posts
Chat
FAQ
IOTW
Tutorials
Bücher
zfxCON
ZFXCE
Mathlib
ASSIMP
NES
Wir über uns
Impressum
Regeln
Suchen
Mitgliederliste
Membername:
Passwort:
Besucher:
4440681
Jetzt (Chat):
19 (0)
Mitglieder:
5239
Themen:
24223
Nachrichten:
234554
Neuestes Mitglied:
-insane-
ZFX - C++ TutorialsDruckversion

C++-Exceptions
Das C++ Tutorials von Christian Mandery aka ChrisM erklärt das grundlegende Konzept der C++-Exceptions.

© Copyright 2003 [whole tutorial] by Christian Mandery aka ChrisM
Die Texte der Tutorials unterliegen dem Copyright und dürfen ohne schriftliche Genehmigung vom Author weder komplett noch auszugsweise vervielfältigt, auf einer anderen Homepage verwendet oder in sonst einer Form veröffentlicht werden. Die zur Verfügung gestellten Quelltexte hingegen stehen zur freien Weiterverwendung in Software Projekten zur Verfügung.
Die Quelltexte dieses Tutorials werden auf einer "as is" Basis bereit gestellt. Der Autor übernimmt keinerlei Garantie für die Lauffähigkeit der Quelltexte. Für eventuelle Schäden die sich aus der Anwendung der Quelltexte ergeben wird keinerlei Haftung übernommen.


C++-Exceptions

Hi,

mit diesem Tutorial oder besser gesagt Kurzartikel möchte ich euch das Konzept der C++ Exceptions etwas näherbringen. Bei dieser Technik handelt es sich um einen Mechanismus, der eine einfache Fehlerbehandlung, z.B. auch in einer 3D-Engine behandelt.

Eines muss ich aber vorher sagen, damit ihr nicht am Ende hinterher enttäuscht seid: Ja, es stimmt, die Verwendung von Exceptions sorgt für einen geringen Overhead in der ausführbaren Executable (am Anfang und Ende jeder Funktion muss Code hinzugefügt werden). Dieser kleine Performanceverlust sollte aber heute nicht mehr allzu viel ausmachen, also spricht eigentlich aus meiner Sicht (und auch der Sicht von vielen anderen Leuten ;) ) nichts mehr dagegen, Exceptions in Spielen und Engines verwenden.

Warum Exceptions?

Um diese Frage zu klären, werden wir uns zuerst mal einen Codeabschnitt anschauen:

bool InitialisiereIrgendwas(...)
{
  if (FAILED(EineFunktion()))  // HRESULT-Test mit FAILED
  {
    Log("EineFunktion() fehlgeschlagen!");
    return false;
  }

  if (!NochEineFunktion())  // Test auf boolschen False-Wert
  {
    Log("NochEineFunktion() fehlgeschlagen!");
    return false;
  }

  return true;
}

Jeder, der mal Band 1 gelesen hat, dürfte solche Codeauszüge recht bekannt finden ... ;)

Der erste Nachteil, der natürlich sofort auffällt, ist natürlich die begrenzte Speichermöglichkeit eines boolschen Wertes (true/false), der keine genaue Beschreibung des Fehlers erlaubt und dem Aufrufer daher keine Möglichkeit gibt, den Fehler zu behandeln (ok, Logdatei zur Laufzeitauslesen, aber das ist ja nicht grade toll *g*). Nun könnte man zwar einen HRESULT verwenden, aber auch der kann nur begrenzt Informationen speichern, ich kann in einem HRESULT, z.B. weder die Quellcodedatei (erhält man durch das Standarddefine __FILE__) des Fehlers, noch die Zeile des Fehlers oder sonstige Informationen wie den Direct3D-Fehlercode speichern.

Zweitens wird durch eine bool/HRESULT-Architektur der Rückgabewert "blockiert", z.B. könnte eine 3D-Modellklasse eine Funktion Render() haben, die die Anzahl der gerenderten Dreiecke zurückgibt. Wird jetzt der Rückgabewert schon für einen Fehlercode benutzt, müsste man diese Zahl irgendwo anders speichern, z.B. einen int* übergeben, der auf die Speicheradresse zeigt, was ja alles andere als komfortabel ist.

Drittens sind Fehlercodes bei Berechnungsfunktion wirklich äußerst unbequem. Was macht beispielsweise eine Wurzelfunktion, wenn ich eine negative Zahl als Parameter angebe, oder eine Speicherreservierenfunktion, wenn kein Platz mehr auf dem Heap ist (das C++-Schlüsselwort new wirft glücklicherweise eine Exception [was das ist, klären wir später]: std::bad_alloc)

Der vierte Nachteil ist, dass die Behandlung der Rückgabewerte optional ist, d.h. wenn die Aufruferfunktion unsauber geschrieben ist, wird der Rückgabewert gar nicht erst abgefragt und der Fehler nicht bemerkt. Exceptions muss man abfangen, sonst wird das Programm beendet (dazu kommen wir auch später noch).

Wenn ihr euch noch mal meinen kleinen Beispielcode oben anschaut, seht ihr wahrscheinlich noch einen Nachteil der Rückgabewerte-Fehlercodes: Er besteht aus viel Code und man muss oft das Gleiche oder ähnliche Codepassagen tippen.

Genug Nachteile also für einen neuen Fehlerbehandlungsmechanismus, nämlich die ...

Exceptions

Der Exception-Mechanismus arbeitet mit den drei C++-Schlüsselwörtern catch, throw und try. Ich denke, es ist am besten, wenn ich jetzt hier einfach mal ein Beispiel zeige, dass ich dann danach erkläre:



Wie man sieht, wirft fBerechneWurzel() eine char*-Exception, wenn fZahl (der Parameter der Funktion) negativ ist. Eine Exception kann dabei jedes Objekt sein, also auch eine eigene Klasse, in der man (fast) beliebig viele Informationen über den aufgetretenen Fehler speichern kann, aber dazu werden wir später noch kommen (langsam hass' ich diesen Spruch *lol*)

Wenn nun eine Exception geworfen wird, wird die aktuelle Funktion beenden und der Stack aufgerollt. Diesen Vorgang muss man sich so vorstellen, dass C++ in der übergeordneten Aufruferfunktion nachschaut, ob der Funktionsaufruf in einem try-Block geschehen ist. Falls ja, wird geschaut, ob nach dem try-Block ein catch kommt, dessen Objekttyp dem der Exception entspricht (oder halt catch(...)). In diesem Fall. geht die Programmausführung in der ersten Zeile des catch-Block weiter. Falls kein try-Block oder keine passende catch-Phrase gefunden wird, geht die Suche weiter aufwärts bis sie irgendwann in der WinMain() bzw. main() ankommt. Wird dort dann auch kein try-Block mit passender catch-Phrase gefunden, wird das Programm beendet (im Debugger gibt es dann eine Meldung, wie z.B. "Unknown software exception"). Wichtig ist, dass beim Aufrollen des Stacks alle Objekte auf dem Stack richtig wie bei einem return freigegeben werden, also auch Destruktoren aufgerufen werden.

Das Grundprinzip der Exceptions sollte man nun verstanden haben, das wahre Potential der Exceptions steckt aber darin, dass man Instanzen von eigenen Klassen werfen kann und in der Klasse alles unterbringen kann, was man will. Aber es wird noch besser, ich sag nur Vererbung, aber mehr dazu im nächsten Kapitel...

Klassen als Exception

Ich poste einfach mal den Header von einer Basisklasse, die man zur Fehlerbehandlung einsetzen könnte (die Implementierung könnt ihr euch ja denken, ist nicht schwer :) ). Bitte beachten: Ich hab die Membervariablen hier jetzt aus Faulheit einfach mal public gemacht, aber in einer richtigen Implementierung sollte man sie lieber protected (nicht private, warum sehen wir später!) machen und Zugriffsmethoden (Set*() / Get*()) einbauen.



Also, wie wir ja sehen hat diese Klasse drei Membervariablen, nämlich m_strError (gibt einen kleinen Text an, der den Fehler beschreibt, z.B. "xyz() failed!"), m_strFile (gibt die Datei an, in der der Fehler auftrat) und m_nLine (gibt die Zeilennummer der Quellcodedatei an, in der der Fehler auftrat).

Da es jetzt aber irgendwie nicht besonders bequem ist, wenn man Datei und Zeilennummer immer von Hand tippen muss und vor allem verändern muss, wenn man den Code kopiert oder vornedran noch Zeilen einfügt, können wir unser ENGINEEXCEPTION()-Makro verwenden (der Name ist natürlich egal, ich schreib Makronamen/Defines nur grundsätzlich groß, also nicht erschrecken ;) ). Beispiel: Wir schreiben ENGINEEXCEPTION("Testfehler"); in der Datei c:\test\test.cpp, Zeile 1234 und unser Makro macht daraus throw CException("c:\test\test.cpp", 1234, "Testfehler");. Gut oder? :)

Jetzt haben wir schon eine solide Klasse und könnten sie z.B. so verwenden: (Exceptions kann man übrigens überall werfen, also auch in Konstruktoren etc., nur sollten wir aufpassen, dass die Exceptionklasse nicht selbst eine Exception wirft =) )



Das ist sicherlich schon toll, aber noch lange nicht das komplette Potential der C++-Exceptions. Richtig interessant wird es dann, wenn wir Vererbung und Polymorphimus (vielleicht sagt einigen der Begriff späte Bindung, also Laufzeitbindung mittels VTable mehr, falls euch das nichts sagt: Das ist "einfach" der Mechanismus, den C++ bei virtuellen Funktionen anwendet, um mittels VTABLE den Typ des Objektes und damit die aufzurufende Funktion zu ermitteln) verwenden.

Vererbung von Exceptionklassen

Wieder mal werfe ich einfach etwas Code ins Spiel:

class CDXException : public CException
{
  CDXException(const std::string &strFile, unsigned int nLine, HRESULT hrResult,
    const std::string &strError = "");

  // Die anderen Membervariablen und WriteToLog()/DisplayMessageBox() werden vererbt

  virtual void GenerateText() const;  // <- Unsere virtuelle Funktion!

  HRESULT hrResult;
};

ENGINEDXEXCEPTION(ErrorString, hr) throw CDXException(__FILE__, __LINE__, hr, ErrorString)

Ich denke, den Code muss ich nicht wirklich erklären, nur was es mit der virtuellen Funktion auf sich hat. Der Trick ist ganz einfach folgender: Wenn wir ein CDXException-Objekt als Exception werfen (aus Gründen der Bequemlichkeit und Übersetzungseinheit-/Zeilenautomatik wieder über das neue Makro ;) ) wird es automatisch von unserer catch (CException)-Phrase verarbeitet. Diese ruft dann unter anderem DisplayMessageBox() auf, die ja wiederrum GenerateText() aufruft. Jetzt kommt der Clou: Weil wir die Funktion virtuell deklariert haben, sucht C++ nach Typinformationen und findet heraus, dass die Exception vom Typ CDXException ist. Dann wird nicht CException::GenerateText(), sondern CDXException::GenerateText() aufgerufen und die generiert uns natürlich einen Text für die Messagebox oder den Log, der auch den HRESULT beinhaltet (da es sich ja hier um einen DirectX-Fehlerwert handelt, sollte man ihn vorher z.B. mit DXGetErrorString9() in einen schönen String verwandeln, denn keiner weiß genau, welche Zahl jetzt z.B. D3DERR_INVALIDCALL entspricht. :) ).

Ich denke, die meisten haben jetzt erkannt, wie gut und einfach Exceptions zu verwenden sind: Man kann dutzende von Exceptions einführen und muss trotzdem an der Stelle, wo die Fehler behandelt werden, nichts ändern.

Auch lohnenswert kann es sein, seine Exceptionklassen von den Exceptionklassen der STL oder einfach den Standardexceptionklassen abzuleiten, wie z.B. std::bad_alloc (das wirft new, wenn momentan kein Heapspeicher allokiert werden kann), std::out_of_range (das wirft u.a. ein std::vector bei Indexüberschreitung mit at()-Zugriff) usw., dass bestehender Code die eigenen Exceptions noch leichter verarbeiten kann. Aber ich denke, ihr werdet schon auf genug Ideen kommen, man könnte ja sogar für jeden eigenen Fehlercode eine eigene kleine von der Basisexceptionklasse abgeleitete Klasse erstellen ... wie gesagt, einfach mal mit Exceptions rumprobieren :)

Eines noch, falls sich jemand fragt, warum DirectX keine C++-Exceptions verwendet: Weil DirectX auch noch mit C und Visual Basic verwendbar sein soll. Aber es gibt die so genannten COM Exceptions, einfach mal die Suchfunktion hier verwenden (bitte keine Fragen dazu, ich hab sie noch nie benutzt).

So, ich denke, dass wars eigentlich mit meiner kleinen Einführung in die Welt der C++-Exceptions. ;)

Christian Mandery (Nickname bei ZFX: ChrisM)


Wer Fehler findet oder noch Fragen hat, kann mir gerne eine Private Message hier bei ZFX senden ... :)



Informationen: " ); ?> " ); ?> " ); ?> " ); ?>
WWW :$wwwtitel
Data:Projektdateien
Mail:$author ($email)
Nick:$nick