Fehlerbehandlungsstrategien: Den richtigen Ansatz wählen
Vergleiche Exceptions, Fehlercodes und std::expected — lerne, wann welcher Ansatz angemessen ist und wie Fehler in Konstruktoren, Destruktoren und multithreaded Code behandelt werden.
Keine Universallösung
C++ bietet mehrere Fehlerbehandlungsmechanismen, jeder mit unterschiedlichen Kompromissen. Die richtige Wahl hängt von deiner Domäne ab:
Exceptions sind am besten für Fehler, die selten sind und nicht lokal behandelt werden können. Sie haben null Kosten auf dem Erfolgspfad, sind aber teuer beim Werfen (Tausende von Nanosekunden). Sie propagieren automatisch über Aufrufebenen.
Fehlercodes (Rückgabewerte, errno, std::error_code) sind am besten für Fehler, die erwartet und häufig sind. Sie haben kleine, konstante Kosten bei jedem Aufruf (der Aufrufer muss den Rückgabewert prüfen). Sie sind der Standard in C-APIs und Kernel-Level-Code.
std::expected (C++23) kombiniert das Beste aus beidem: Es zwingt den Aufrufer, den Fehler zur Kenntnis zu nehmen (wie Fehlercodes), trägt aber einen reichhaltigen Fehlertyp (wie Exceptions) und unterstützt monadische Verkettung. Es wird schnell zum bevorzugten Ansatz in neuem C++-Code.
Die Schlüsselerkenntnis: Dies sind keine konkurrierenden Philosophien — es sind Werkzeuge für unterschiedliche Situationen, und Produktionscode verwendet oft alle drei.
Vergleich: Exceptions vs. Fehlercodes vs. std::expected
| Kriterium | Exceptions | Fehlercodes | std::expected |
|---|---|---|---|
| Kosten bei Erfolg | Null (tabellenbasiert) | Branch + Prüfung | Minimal (speichert Wert) |
| Kosten bei Fehler | Hoch (Unwinding) | Wie bei Erfolg | Wie bei Erfolg |
| Kann ignoriert werden? | Nein (Programm terminiert) | Ja (stille Fehler) | Schwieriger (muss .value() aufrufen) |
| Konstruktoren | Einzige Option | Kann keinen Wert zurückgeben | Kann keinen Wert zurückgeben |
| Destruktoren | Darf nicht werfen | In Ordnung | In Ordnung |
| Komponierbarkeit | Begrenzt (try/catch) | Manuelle if-Ketten | Monadisch (and_then, transform) |
| Binärgröße | Größer (Unwind-Tabellen) | Minimal | Minimal |
| Echtzeit-tauglich? | Nein (unbegrenzte Zeit) | Ja | Ja |
Fehlerbehandlung in Konstruktoren
Konstruktoren können keine Fehlercodes zurückgeben — sie haben keinen Rückgabewert. Exceptions sind der einzige saubere Mechanismus, um Konstruktionsfehler zu signalisieren. Eine geworfene Exception bedeutet, dass das Objekt nie vollständig konstruiert wurde, daher wird sein Destruktor nie aufgerufen (aber Destruktoren bereits konstruierter Unterobjekte und Basen werden aufgerufen).
#include <fstream>
#include <string>
#include <stdexcept>
#include <iostream>
class ConfigFile {
public:
explicit ConfigFile(const std::string& path)
: path_(path), stream_(path) {
if (!stream_.is_open()) {
throw std::runtime_error(
"Cannot open config file: " + path);
}
// Parse the file — if this throws, stream_ is
// already constructed and its destructor will
// close the file automatically (RAII)
parse();
}
std::string get(const std::string& key) const {
auto it = data_.find(key);
if (it == data_.end()) {
throw std::out_of_range("Key not found: " + key);
}
return it->second;
}
private:
void parse() {
std::string line;
while (std::getline(stream_, line)) {
auto pos = line.find('=');
if (pos != std::string::npos) {
data_[line.substr(0, pos)] = line.substr(pos + 1);
}
}
}
std::string path_;
std::ifstream stream_; // RAII: closed by destructor
std::map<std::string, std::string> data_;
};
// Usage: the object is fully valid or does not exist
int main() {
try {
ConfigFile cfg("/etc/myapp.conf");
std::cout << cfg.get("port") << '\n';
} catch (const std::exception& e) {
std::cerr << e.what() << '\n';
}
}std::error_code für System-Level-Fehler
std::error_code bietet einen leichtgewichtigen, nicht-werfenden Mechanismus für System-Level-Fehler. Die -Bibliothek ist ein großartiges Beispiel — die meisten Funktionen haben eine Überladung, die einen std::error_code&-Parameter nimmt, anstatt zu werfen.
#include <filesystem>
#include <iostream>
#include <system_error>
namespace fs = std::filesystem;
// Throwing version — suitable for unexpected errors
void copy_file_throwing(const fs::path& src, const fs::path& dst) {
fs::copy_file(src, dst); // throws fs::filesystem_error on failure
}
// Non-throwing version — suitable for expected failures
bool copy_file_safe(const fs::path& src, const fs::path& dst) {
std::error_code ec;
fs::copy_file(src, dst, ec); // sets ec instead of throwing
if (ec) {
std::cerr << "Copy failed: " << ec.message()
<< " (" << ec.category().name()
<< ':' << ec.value() << ")\n";
return false;
}
return true;
}
int main() {
// The error_code version is preferable in loops where
// failure is expected (e.g., batch file processing)
for (const auto& entry : fs::directory_iterator(".")) {
std::error_code ec;
auto sz = fs::file_size(entry.path(), ec);
if (!ec) {
std::cout << entry.path().filename()
<< ": " << sz << " bytes\n";
}
}
}Destruktoren sind seit C++11 implizit noexcept. Wenn ein Destruktor wirft, wird std::terminate() aufgerufen. Selbst wenn du einen Destruktor explizit mit noexcept(false) markierst, führt das Werfen während des Stack Unwinding (wenn bereits eine andere Exception aktiv ist) zu std::terminate().
- Loggen und unterdrücken: Wenn eine Bereinigungsoperation fehlschlägt, logge den Fehler und fahre fort
- Eine separate
close()-Methode bereitstellen: Lass Aufrufer Bereinigungsfehler explizit behandeln, bevor die Zerstörung stattfindet - Ressourcen so entwerfen, dass die Bereinigung nicht fehlschlagen kann: Zum Beispiel gelingt das Schließen eines File-Deskriptors auf OS-Ebene immer
- Niemals eine werfende Funktion aus einem Destruktor aufrufen, ohne sie in einen try/catch zu verpacken
Fehlerbehandlung in multithreaded Code
Exceptions überqueren keine Thread-Grenzen. Wenn ein Thread wirft und die Exception nicht innerhalb dieses Threads gefangen wird, wird std::terminate() aufgerufen. Um Exceptions zwischen Threads zu propagieren:
1. Verwende std::future und std::async: Exceptions, die im asynchronen Task geworfen werden, werden erfasst und beim Aufruf von .get() auf dem Future erneut geworfen.
2. Verwende std::exception_ptr mit std::current_exception() und std::rethrow_exception(), um Exceptions manuell zu erfassen und zu transportieren.
3. Verwende std::promise::set_exception(), um eine Exception von einem Produzenten-Thread an einen Konsumenten zu senden.
Für hochdurchsatzorientierten nebenläufigen Code werden Fehlercodes oder std::expected oft bevorzugt, weil sie die Kosten der Exception-Infrastruktur in jedem Thread vermeiden und einfacher über Thread-Grenzen hinweg zu verstehen sind.
Passe den Fehlerbehandlungsmechanismus an die Situation an.
- Verwende Exceptions für Fehler, die selten sind, nicht lokal behandelt werden können und den Stack hinauf propagieren sollten (z.B. Datei nicht gefunden, Datenbank nicht erreichbar)
- Verwende Fehlercodes für hochfrequente Operationen, bei denen Fehler normal und erwartet sind (z.B. Cache-Lookup-Fehlschlag, nicht-blockierende E/A würde blockieren)
- Verwende
std::expectedfür neuen Code, bei dem du explizite Fehlerbehandlung mit reichhaltigen Fehlertypen und monadischer Komposition möchtest - Verwende Exceptions in Konstruktoren — sie sind der einzige Weg, Fehler zu signalisieren, bevor das Objekt existiert
- Wirf niemals in Destruktoren — logge den Fehler und unterdrücke ihn
- Exceptions, Fehlercodes und
std::expectedsind komplementäre Werkzeuge — keine konkurrierenden Philosophien - Konstruktoren müssen Exceptions verwenden, um Fehler zu signalisieren; Fehlercodes sind keine Option
- Destruktoren dürfen niemals werfen — sie sind seit C++11 implizit
noexcept - Exceptions überqueren keine Thread-Grenzen — verwende
std::futureoderstd::exception_ptr, um sie zu übertragen std::error_codeist ideal für System-Level-Fehler, bei denen du eine nicht-werfende API möchtest
Quiz — Teste dein Wissen
(15 XP)1. Warum sind Exceptions der bevorzugte Fehlerbehandlungsmechanismus in Konstruktoren?
2. Was passiert, wenn eine Exception in einem Destruktor während des Stack Unwinding von einer anderen Exception geworfen wird?
3. Wie können Exceptions zwischen Threads übertragen werden?