Wie bereits erwähnt ist das zentrale Element der Fehlerbehandlung das Objekt der Exception. In C++ kann eine Exception jeden beliebigen Typ annehmen, also sowohl einen primitiven Datentyp wie int
oder const char*
, oder aber auch beliebige Klassen oder Strukturen.
Um eine Exception auszulösen gibt es das Schlüsselwort throw
(=werfen), welches gefolgt von einem Fehlerobjekt dieses in den Raum „wirft“, um damit einen Fehler zu signalisieren. Schauen wir uns dazu einmal ein einfaches Beispiel an:
int main(int argc, char* argv[]) { throw 123; return 0; }
Dieses Programm tut nichts weiter als ein Exception-Objekt vom Typ int
mit dem Wert 123
in den Raum zu „werfen“, und nachdem wir noch nicht dafür gesorgt haben, dass es irgendwo wieder aufgefangen wird, kommt die Funktion std::terminate() ins Spiel, die in einem solchen Fall aufgerufen wird und das Programm mit einem Aufruf von std::abort() sofort beendet. Je nachdem welche Implementierung der Standardbibliothek man verwendet kann auch noch zusätzlich ein Hinweis auf den Grund des Beendens des Programms ausgegeben werden.
Unter Ubuntu mit der GNU ISO C++ Library erhalte ich zum Beispiel folgende Ausgabe:
terminate called after throwing an instance of 'int' Aborted
Wie bereits erwähnt kann man beliebige Objekte als Exception werfen. Das throw-Statement könnte als auch eine der folgenden Formen annehmen:
char const*
throw "Hello exception world"; // Hier wird eine Exception vom Typ char const* geworfen
double
throw 1.3; // Achtung! Der Typ ist in diesem Fall double und nicht float! // Für float müsste man 'throw 1.3f;' schreiben
SimpleError
struct SimpleError{}; // ... throw SimpleError;
// auch mit erweiterten Informationen: struct ErrorWithInfo { ErrorWithInfo(int error_code): _error_code(error_code) {} int _error_code; } // ... throw ErrorWithInfo(123);
Nachdem es aber wenig Sinn macht einfach Exceptions in den Raum zu werfen, und damit das Programm zu beenden, gibt es die Möglichkeit Exceptions aufzufangen um einerseits den Fehler zu behandeln und andererseits das Programm wieder normal, oder zumindest so gut wie es der aufgetreten Fehler erlaubt, weiterlaufen zu lassen.
Dabei kennzeichnen wir einen Block in dem Fehler auftreten können mit try
und schreiben anschließend ein oder mehr catch
-Blöcke, die jeweils einen Typ von Exception-Objekten auffangen können. Bei einem Fehler springt die Ausführung des Programms in den entsprechenden catch
-Block und wird anschließend nach dem letzten catch
-Block fortgesetzt.
#include <iostream> // Klassen für Fehler struct error {}; struct out_of_mem_error : error {}; int main(int argc, char* argv[]) { std::cout << "Vorher..." << std::endl; try // Überwachung für diesen Block aktivieren { std::cout << __LINE__ << std::endl; throw 32; // Einen Integer als Fehler werfen std::cout << __LINE__ << std::endl; throw out_of_mem_error; // Und jetzt out_of_mem_error als Fehler std::cout << __LINE__ << std::endl; throw error; std::cout << __LINE__ << std::endl; throw "Error: Failed!!!"; std::cout << __LINE__ << std::endl; throw 2.4; std::cout << __LINE__ << std::endl; } catch( int ex ) { std::cout << "int: " << ex << std::endl; } catch( out_of_mem_error& ex ) { std::cout << "out_of_mem_error" << std::endl; } catch( error& ex ) { std::cout << "error" << std::endl; } catch( char const* ex ) { std::cout << "[ERROR]: " << ex << std::endl; } catch(...) // Ellipsis-Handler fängt alles { std::cout << "Unknown error" << std::endl; } std::cout << "Nachher..." << std::endl; return 0; }
An diesem nicht gerade sinnvollen Code kann man sehr gut das Verhalten von Exceptions beobachten und damit experimentieren. Wenn wir das jetzt kompilieren und ausführen wird nach der Ausgabe von „Vorher…“ ein Integer (32) als Exception geworfen. Dadurch wird die Codeausführung an dieser Stelle unterbrochen und nach dem try
-Block der erste, für diesen Typ passende catch
-Handler aufgerufen. In unserem Fall passt bereits der erste und die Ausführung des Codes wird nach dem letzten catch
-Block fortgesetzt. An dieser Stelle würde ich empfehlen mit dem Code zu experimentieren und von oben nach unten, der Reihe nach die throw
-Statements auskommentieren um zu sehen wie in die verschiedenen catch
-Handler gesprungen wird.
Besonders beachten sollten wir auch wie beim Fangen mit Vererbung umgegangen wird. Wenn wir Exceptions nämlich mit Referenzen fangen, dann verhalten sich die Handler polymorph - wir können also mit einer Referenz auf eine Klasse, alle von ihr abgeleiteten Klassen fangen. Wichtig ist dabei die Reihenfolge der Handler zu beachten und spezialisierte Klassen vor den weniger spezialisierten Klassen zu fangen, da diese sonst nie eine entsprechend Exception erhalten können.
Eine Besonderheit stellt noch der letzte Handler da, bei dem es sich um einen sogenannten Ellipsis-Handler (catch(…)
) handelt. Dieser kann jeden beliebigen Typ von Exception fangen, sollte aber aus diesem Grund auch wirklich nur in Ausnahmesituationen und ansonsten vermieden werden.
↑ Exceptions Start ↑ | → std::exception - Exceptions in der Standardbibliothek →