Exceptionbehandlung in C++

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.

throw - Exceptions werfen...

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:

  • Text: Exception-Objekt ist vom Typ char const*
    throw "Hello exception world"; // Hier wird eine Exception vom Typ char const* geworfen
  • Gleitkommazahlen: zb. 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
  • Strukturen oder Klassen: Exception Typ ist hier SimpleError
    struct SimpleError{};
    // ...
    throw SimpleError;
  • Strukturen und Klassen dürfen natürlich auch einen Konstruktor und Member haben:
    // auch mit erweiterten Informationen:
    struct ErrorWithInfo
    {
      ErrorWithInfo(int error_code):
        _error_code(error_code)
      {}
     
      int _error_code;
    }
    // ...
    throw ErrorWithInfo(123);

try/catch - ...und wieder fangen

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

Diskussion