Fehlerbehandlung in C++: von try/catch bis RAII und noexcept
Die Zuverlässigkeit eines Programms hängt direkt davon ab, wie es auf unvorhergesehene Situationen reagiert. In C++ sind Ausnahmen der primäre Mechanismus dafür. In diesem Artikel analysieren wir die klassische Konstruktion try/catch/throw und betrachten auch alternative und moderne Ansätze: RAII, noexcept und Fehlerbehandlung ohne Ausnahmen.
1. Basismechanismus: try, catch, throw
Eine Ausnahme ist ein Objekt, das beim Auftreten eines Fehlers "geworfen" (throw) wird. Das Programm kann es abfangen (catch) und korrigierende Maßnahmen durchführen, ohne die Arbeit vollständig zu unterbrechen.
Syntax
#include <iostream>#include <stdexcept>
double divide(double a, double b) { if (b == 0.0) { throw std::runtime_error("Division durch Null!"); } return a / b;}
int main() { try { double result = divide(10.0, 0.0); std::cout << "Ergebnis: " << result << std::endl; } catch (const std::runtime_error& e) { std::cerr << "Fehler: " << e.what() << std::endl; } return 0;}Arbeitsregeln
- throw — erstellt eine Kopie des Objekts und übergibt die Kontrolle an den Handler.
- try — Codeblock, in dem Ausnahmen auftreten können.
- catch — fängt die Ausnahme nach Typ ab. Es können mehrere
catch-Blöcke angegeben werden. - Wenn eine Ausnahme nicht abgefangen wird, wird
std::terminate()aufgerufen.
Abfangen nach Basisklasse
Alle Standardausnahmen erben von std::exception. Daher kann man einen universellen Handler schreiben:
try { // Code} catch (const std::exception& e) { std::cerr << "Standardfehler: " << e.what() << std::endl;} catch (...) { std::cerr << "Unbekannte Ausnahme" << std::endl;}Der Block catch (...) fängt grundsätzlich alle Ausnahmen, jedoch ohne Zugriff auf das Objekt.
2. Ausnahmen in Konstruktoren und Destruktoren
Konstruktoren
Wenn ein Konstruktor eine Ausnahme wirft, gilt das Objekt als nicht erstellt, und der Destruktor wird für es nicht aufgerufen. Dies ist wichtig für die Ressourcenverwaltung. Beispiel:
class Resource {public: Resource() { std::cout << "Ressource zugewiesen" << std::endl; throw std::runtime_error("Initialisierungsfehler"); } ~Resource() { std::cout << "Ressource freigegeben" << std::endl; }};
int main() { try { Resource r; } catch (...) { std::cout << "Ausnahme abgefangen" << std::endl; } // Der Destruktor von Resource wird NICHT aufgerufen! return 0;}Destruktoren
Ausnahmen aus einem Destruktor zu werfen ist schlechte Praxis. Wenn der Destruktor während der Stack-Abwicklung (stack unwinding) aufgerufen wird und er eine neue Ausnahme wirft, wird das Programm abrupt beendet. Schreiben Sie Destruktoren immer als noexcept (standardmäßig sind sie das bereits).
3. RAII — Automatische Ressourcenverwaltung
Die Idiom RAII (Resource Acquisition Is Initialization) ist der Schlüssel zu sicherem Code. Eine Ressource (Speicher, Datei, Mutex) wird im Konstruktor erfasst und im Destruktor freigegeben. Wenn eine Ausnahme auftritt, werden die Destruktoren aller erstellten Objekte automatisch aufgerufen (stack unwinding), und die Ressourcen werden freigegeben.
Beispiel mit dem intelligenten Zeiger std::unique_ptr:
#include <memory>#include <iostream>
void process() { auto ptr = std::make_unique<int>(42); // Wenn hier eine Ausnahme auftritt, // gibt unique_ptr den Speicher automatisch frei throw std::runtime_error("Fehler in proc");