Menu

C++-Ausnahmen: throw, what() und Fehlerbehandlung

Ausnahmen melden Fehler, die eine Funktion lokal nicht behandeln kann. Lerne, wie man throw verwendet, welche Standard-Ausnahmetypen es gibt, was die what()-Nachricht ist und warum Ausnahmen bei den wirklich wichtigen Fehlern besser sind als Rückgabecodes.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Warum es Ausnahmen gibt

Auf der vorherigen Seite hast du enum class verwendet, um Fehlerzuständen aussagekräftige Namen zu geben. Das ist großartig für Ergebnisse, mit denen eine Funktion rechnet und die der Aufrufer prüfen soll. Doch manche Fehler sind anders: Eine Funktion tief in deinem Aufrufstapel stellt fest, dass sich eine Datei nicht öffnen lässt oder dass ein Argument keinen Sinn ergibt, und sie hat keine Ahnung, was das Programm dagegen tun sollte. Einen Fehlercode zurückzugeben funktioniert nur, wenn jeder Aufrufer in der Kette daran denkt, ihn zu prüfen und weiterzureichen. Verpasst du auch nur eine einzige Prüfung, segelt das Programm mit Datenmüll weiter.

Ausnahmen lösen das. Wenn etwas schiefgeht, wirfst du mit throw ein Objekt. Die Ausführung stoppt sofort, der Stack wird abgewickelt (für jedes lokale Objekt zwischen dem Wurf und dem Handler wird der Destruktor ausgeführt) und die Kontrolle springt zum nächsten passenden catch. Eine unbehandelte Ausnahme kann nicht stillschweigend ignoriert werden: Fängt sie nichts ab, ruft das Programm std::terminate auf und bricht ab.

Diese Seite konzentriert sich auf die werfende Seite – die Fehlerobjekte selbst. Die nächste Seite geht ausführlich auf die try/catch-Mechanik ein.

Das Werfen und die what()-Nachricht

Technisch kannst du jeden Wert mit throw werfen —throw 42; oder throw "oops"; sind erlaubt— aber lass es. Die Konvention, der alle folgen, ist, ein von std::exception abgeleitetes Objekt zu werfen. Diese Basisklasse deklariert eine einzige virtuelle Methode, what(), die eine const char*-Beschreibung des Problems zurückgibt. Sich an die Konvention zu halten bedeutet, dass ein einziges catch (const std::exception& e) alles behandeln kann.

Der Header <stdexcept> liefert dir fertige Typen, deren Konstruktor die Nachricht entgegennimmt:

Beachte, dass what() genau die Zeichenkette zurückgibt, mit der du die Ausnahme konstruiert hast. Beachte außerdem, dass wir sie mit const exception& abgefangen haben, obwohl wir einen runtime_error geworfen haben – das funktioniert, weil ein runtime_error ein std::exception ist (eine Beziehung, die du von der Seite über Vererbung wiedererkennst).

Die Standard-Ausnahmehierarchie

Bevor du deinen eigenen Ausnahmetyp schreibst, prüfe, ob die Standardbibliothek bereits einen passenden hat. Sie alle erben von std::exception und teilen sich in <stdexcept> in zwei Familien auf:

  • logic_error – ein Fehler in der Programmlogik, der sich im Prinzip schon vor der Ausführung erkennen ließe. Zu den Untertypen gehören invalid_argument, out_of_range, domain_error und length_error.
  • runtime_error – ein Fehlschlag, der erst zur Laufzeit auftritt und an sich kein Programmierfehler ist. Zu den Untertypen gehören range_error, overflow_error und underflow_error.

Viele Bibliotheksfunktionen werfen diese für dich. Zum Beispiel führt std::vector::at() eine Bereichsprüfung durch und wirft out_of_range, anstatt dich über das Ende hinaus lesen zu lassen:

Dieses at() ist das sichere Gegenstück zu v[9]. Der schlichte operator[] führt keine Bereichsprüfung durch: v[9] hier zu lesen ist undefiniertes Verhalten, keine Ausnahme. at() zu wählen ist der Weg, eine stille Datenverfälschung in einen abfangbaren Fehler zu verwandeln.

Wähle den Typ, der den Fehler beschreibt: invalid_argument, wenn ein Aufrufer etwas Unsinniges übergibt, out_of_range für Index-/Schlüsselprobleme, runtime_error für "die Außenwelt hat mich im Stich gelassen".

Einen eigenen Ausnahmetyp schreiben

Wenn kein Standardtyp passt – du willst zusätzliche Daten anhängen oder gezielt nur deinen Fehler und nichts anderes mit catch abfangen – definiere eine Klasse, die von std::exception (oder einem seiner Untertypen) erbt, und überschreibe what(). Von std::runtime_error zu erben ist der einfachste Weg, weil er die Nachricht bereits speichert und what() für dich implementiert:

Da NetworkError einen Statuscode mit sich trägt, kann der Handler darauf reagieren: bei einem 5xx erneut versuchen, bei einem 4xx aufgeben. Eine bloße Fehlerzeichenkette könnte das nicht. Der eigene Typ erlaubt es außerdem einem catch (const NetworkError&), nur Netzwerkprobleme abzufangen und alles andere dem allgemeineren Handler darunter zu überlassen.

Falls du jemals direkt von std::exception (nicht von runtime_error) erbst, denke daran, what() selbst zu überschreiben und es als noexcept zu markieren, damit es zur Signatur der Basis passt:

class ParseError : public std::exception {
public:
    const char* what() const noexcept override {
        return "failed to parse input";
    }
};

Per Wert werfen, per Referenz fangen

Das ist die wichtigste Regel der C++-Ausnahmen und diejenige, die Anfänger falsch machen. Wirf Objekte per Wert und fange sie per const-Referenz.

throw runtime_error("oops");            // per Wert - korrekt
catch (const runtime_error& e) { ... }  // per const-Referenz - korrekt

Stattdessen per Wert zu fangen —catch (std::exception e)— kopiert die Ausnahme in ein Objekt der Basisklasse und schneidet den abgeleiteten Teil ab. Nach dem Abschneiden ruft e.what() die Implementierung der Basis auf, nicht deine überschriebene, sodass deine sorgfältig formulierte Nachricht verschwindet:

try {
    throw NetworkError(503, "service unavailable");
} catch (std::exception e) {       // per Wert - Object Slicing!
    std::cout << e.what();         // generische Nachricht, status() ist weg
}

Die Referenz (&) bewahrt den echten dynamischen Typ, sodass das virtuelle what() korrekt aufgelöst wird und du weiterhin auf die abgeleiteten Member zugreifen kannst. Füge const hinzu, weil du die Ausnahme nur liest, nicht veränderst. Wirf niemals einen Zeiger (throw new runtime_error(...)): der Fänger müsste ihn mit delete freigeben – aber auf welchem Codepfad? Genau das ist das Leck, das Ausnahmen eigentlich verhindern sollen.

Weiter: try-catch

Du kannst jetzt wohlgeformte Ausnahmen erzeugen und mit throw werfen und für jeden Fehler den richtigen Standardtyp wählen. Die andere Hälfte der Geschichte ist die fangende Seite. Die nächste Seite behandelt try/catch vollständig: das Ordnen mehrerer catch-Blöcke vom spezifischsten zum allgemeinsten, das Auffang-catch (...), das erneute Werfen mit einem nackten throw; und wie RAII (denk an die Smart Pointer zurück) garantiert, dass deine Ressourcen freigegeben werden, während der Stack abgewickelt wird.

Häufig gestellte Fragen

Was ist eine Ausnahme in C++?

Eine Ausnahme ist ein Objekt, das einen Fehler signalisiert, den die aktuelle Funktion nicht allein behandeln kann. Du wirfst sie mit throw, der Stack wird abgewickelt (wobei unterwegs lokale Objekte zerstört werden) und ein passender catch-Block weiter oben übernimmt. Das trennt den Code, der ein Problem erkennt, von dem Code, der entscheidet, was dagegen zu tun ist.

Was ist der Unterschied zwischen throw und return bei Fehlern?

Ein return-Wert muss vom Aufrufer geprüft werden, und das wird leicht vergessen – das Programm macht einfach mit fehlerhaften Daten weiter. Eine geworfene Ausnahme kann nicht ignoriert werden: Fängt sie niemand ab, wird das Programm beendet. Ausnahmen sind für echte Fehlschläge gedacht (eine Datei lässt sich nicht öffnen, eine Eingabe ist ungültig); Rückgabewerte sind weiterhin richtig für normale Ergebnisse, einschließlich der erwarteten "nicht gefunden"-Fälle.

Was macht die Methode what() bei C++-Ausnahmen?

Jede von std::exception abgeleitete Klasse stellt eine virtuelle what()-Methode bereit, die einen const char* mit der Fehlerbeschreibung zurückgibt. Wenn du eine Ausnahme abfängst, liefert dir der Aufruf von e.what() die für Menschen lesbare Nachricht, die du protokollieren oder ausgeben kannst. Die Standard-Ausnahmetypen setzen sie aus der Zeichenkette, die du ihrem Konstruktor übergibst.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S