Ausnahmen: throw, try, catch
Verstehe den C++-Exception-Mechanismus — Stack Unwinding, die Standard-Exception-Hierarchie, benutzerdefinierte Exceptions und wann Exceptions gegenüber Fehlercodes zu bevorzugen sind.
Warum es Exceptions gibt
Vor Exceptions signalisierte C-Code Fehler durch Rückgabe spezieller Werte (wie -1 oder NULL). Dies hatte einen kritischen Mangel: Aufrufer konnten den Fehler stillschweigend ignorieren. Exceptions lösen dies, indem sie das Programm zwingen, den Fehler entweder zu behandeln oder zu terminieren. Sie trennen auch den Fehlererkennungsort (tief im Bibliothekscode) vom Fehlerbehandlungsort (im Anwendungscode), die viele Aufrufebenen voneinander entfernt sein können.
C++-Exceptions folgen einem "Zero-Cost auf dem Erfolgspfad"-Modell auf den meisten modernen Compilern (unter Verwendung von tabellenbasiertem Unwinding). Wenn keine Exception geworfen wird, gibt es keinen Laufzeit-Overhead — keine versteckten if-Prüfungen, keine verfehlten Branch Predictions. Die Kosten fallen nur an, wenn eine Exception tatsächlich geworfen wird, was Exceptions ideal für außergewöhnliche (seltene) Fehlerbedingungen macht.
throw, try und catch
Der Kernmechanismus ist unkompliziert: throw ein Objekt, um einen Fehler zu signalisieren, umschließe riskanten Code mit einem try-Block und catch passende Exception-Typen.
#include <iostream>
#include <stdexcept>
#include <string>
double divide(double numerator, double denominator) {
if (denominator == 0.0) {
throw std::invalid_argument("denominator cannot be zero");
}
return numerator / denominator;
}
int main() {
try {
double result = divide(10.0, 0.0);
std::cout << "Result: " << result << '\n';
} catch (const std::invalid_argument& e) {
std::cerr << "Invalid argument: " << e.what() << '\n';
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << '\n';
} catch (...) {
std::cerr << "Unknown error occurred\n";
}
}Stack Unwinding im Detail
Wenn eine Exception geworfen wird, führt die Laufzeitumgebung Stack Unwinding durch: Sie geht den Aufrufstapel zurück und zerstört alle lokalen Variablen in jedem Frame, indem deren Destruktoren aufgerufen werden. Deshalb ist RAII (Resource Acquisition Is Initialization) so wichtig — wenn du Ressourcen in RAII-Wrappern hältst, werden sie während des Unwinding automatisch freigegeben.
Der Unwinding-Prozess funktioniert wie folgt:
1. Der throw-Ausdruck erstellt das Exception-Objekt (normalerweise in einem speziellen Speicherbereich, nicht auf dem Stack).
2. Die Laufzeitumgebung sucht in der aktuellen Funktion nach einem passenden catch-Handler.
3. Wenn keiner gefunden wird, verlässt sie die aktuelle Funktion und zerstört alle lokalen Objekte in umgekehrter Konstruktionsreihenfolge.
4. Sie wiederholt die Schritte 2-3 den Aufrufstapel hinauf.
5. Wenn in keinem Frame ein Handler gefunden wird, wird std::terminate() aufgerufen.
Schlüsselregel: Wirf per Wert, fange per const-Referenz. Per Wert zu werfen vermeidet Dangling-Zeiger. Per const-Referenz zu fangen vermeidet Slicing (bei dem eine abgeleitete Exception in ein Basisklassenobjekt kopiert wird und die abgeleiteten Daten verloren gehen) und unnötige Kopien.
Die Standard-Exception-Hierarchie
Die Standardbibliothek bietet eine Hierarchie mit std::exception als Wurzel. Sie zu verstehen hilft dir, den richtigen Typ zu werfen und auf der richtigen Ebene zu fangen.
// Standard exception hierarchy (simplified)
//
// std::exception
// ├── std::logic_error (programmer mistakes - precondition violations)
// │ ├── std::invalid_argument
// │ ├── std::domain_error
// │ ├── std::length_error
// │ └── std::out_of_range
// ├── std::runtime_error (errors detectable only at runtime)
// │ ├── std::range_error
// │ ├── std::overflow_error
// │ ├── std::underflow_error
// │ └── std::system_error (OS-level errors with error_code)
// └── std::bad_alloc (memory allocation failure)
// std::bad_cast (failed dynamic_cast)
// std::bad_typeid (typeid on null pointer)
#include <stdexcept>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3};
try {
// .at() throws std::out_of_range, unlike operator[] which is UB
int val = v.at(10);
} catch (const std::out_of_range& e) {
std::cerr << "Out of range: " << e.what() << '\n';
}
}Benutzerdefinierte Exception-Klassen
Leite von std::exception oder einer ihrer Unterklassen ab, um domänenspezifische Exceptions zu erstellen. Überschreibe immer what(), um eine aussagekräftige Nachricht bereitzustellen.
#include <stdexcept>
#include <string>
#include <iostream>
class FileParseError : public std::runtime_error {
public:
FileParseError(const std::string& filename, int line, const std::string& msg)
: std::runtime_error(
"Parse error in '" + filename + "' at line " +
std::to_string(line) + ": " + msg),
filename_(filename),
line_(line) {}
const std::string& filename() const noexcept { return filename_; }
int line() const noexcept { return line_; }
private:
std::string filename_;
int line_;
};
void parse_config(const std::string& filename) {
// Simulate a parse failure at line 42
throw FileParseError(filename, 42, "unexpected token '@@'");
}
int main() {
try {
parse_config("app.conf");
} catch (const FileParseError& e) {
std::cerr << e.what() << '\n';
std::cerr << " File: " << e.filename()
<< ", Line: " << e.line() << '\n';
}
}noexcept und Exception-Spezifikationen
Der noexcept-Spezifizierer teilt dem Compiler (und menschlichen Lesern) mit, dass eine Funktion niemals eine Exception werfen wird. Wenn eine noexcept-Funktion doch wirft, wird std::terminate() sofort aufgerufen — es gibt kein Stack Unwinding.
Warum ist noexcept wichtig?
- Performance: Der Compiler kann die Unwinding-Tabellen für noexcept-Funktionen weglassen und kompakteren Code erzeugen.
- STL-Optimierung: Container wie std::vector werden Elemente während der Reallokation nur dann verschieben, wenn der Move-Konstruktor noexcept ist. Andernfalls fallen sie auf Kopieren zurück, um die starke Exception-Garantie aufrechtzuerhalten. Dies kann ein dramatischer Leistungsunterschied sein.
- Dokumentation: Es ist ein Vertrag mit Aufrufern, dass die Funktion nicht fehlschlagen kann.
Verwende noexcept bei: Move-Konstruktoren, Move-Zuweisungsoperatoren, Swap-Funktionen, Destruktoren (implizit noexcept), einfachen Gettern und Vergleichsoperatoren.
Die alte throw()-Spezifikation (und throw(type-list)) wurde in C++11 als veraltet markiert und in C++20 entfernt. Verwende immer noexcept stattdessen.
Wirf niemals Exceptions in Destruktoren. Während des Stack Unwinding von einer anderen Exception führt das Werfen einer zweiten Exception zum Aufruf von std::terminate(). Wenn ein Destruktor eine Operation durchführen muss, die fehlschlagen kann, fange die Exception innerhalb des Destruktors und behandle sie dort (z.B. loggen und unterdrücken).
- Per Wert fangen schneidet abgeleitete Exceptions ab — fange immer per
const& - Zeiger werfen (z.B.
throw new std::runtime_error(...)) führt zu Speicherlecks — wirf per Wert ...zuerst fangen verbirgt alle spezifischen Handler darunter — setze es an die letzte Stelle- Exceptions in Destruktoren während des Stack Unwinding verursachen
std::terminate() - Exceptions für Kontrollfluss verwenden (nicht-Fehler-Pfade) untergräbt das Zero-Cost-Modell und schadet der Performance
- Wirf per Wert, fange per
const-Referenz, um Slicing und Lecks zu vermeiden - Stack Unwinding zerstört lokale Objekte in umgekehrter Konstruktionsreihenfolge — RAII stellt die Bereinigung sicher
- C++-Exceptions sind Zero-Cost auf dem Erfolgspfad (tabellenbasiertes Unwinding), teuer nur beim Werfen
noexceptermöglicht Compiler-Optimierungen und ist kritisch für Move-Operationen, die von der STL verwendet werden- Leite benutzerdefinierte Exceptions von
std::runtime_erroroderstd::logic_errorab und überschreibewhat()
Quiz — Teste dein Wissen
(15 XP)1. Warum solltest du Exceptions per `const&` anstatt per Wert fangen?
2. Was passiert, wenn eine als `noexcept` markierte Funktion eine Exception wirft?
3. Was passiert während des Stack Unwinding mit lokalen Variablen in jedem Stack Frame?