Exception-Sicherheitsgarantien
Lerne die drei Stufen der Exception-Sicherheit — grundlegend, stark und No-Throw — und wie RAII und Copy-and-Swap sie bereitstellen.
Die drei Garantien
Exception-Sicherheit bedeutet nicht, Exceptions zu verhindern — es geht darum zu definieren, was dein Code garantiert, wenn eine Exception auftritt. Jede Funktion, die du schreibst, bietet implizit eine dieser Stufen:
1. No-Throw-Garantie (stärkste): Die Funktion wirft nie. Sie gelingt immer. Beispiele: Destruktoren, swap, einfache Getter. Mit noexcept markiert.
2. Starke Garantie (Commit-or-Rollback): Wenn die Funktion wirft, ist der Programmzustand genau wie vor dem Aufruf. Es ist, als wäre die Funktion nie aufgerufen worden. Denke an eine Datenbanktransaktion, die entweder vollständig committet oder vollständig zurückrollt.
3. Grundlegende Garantie (minimales Akzeptables): Wenn die Funktion wirft, ist das Programm in einem gültigen, aber nicht spezifizierten Zustand. Keine Ressourcen lecken, alle Invarianten gelten, aber der genaue Zustand kann sich von dem vor dem Aufruf unterscheiden.
Es gibt auch eine vierte, inakzeptable Stufe: keine Garantie — bei der eine Exception das Programm in einem beschädigten Zustand mit geleckten Ressourcen hinterlassen kann. Produktionscode darf niemals weniger als die grundlegende Garantie bieten.
RAII: Das Fundament der Exception-Sicherheit
RAII (Resource Acquisition Is Initialization) bindet die Lebensdauer einer Ressource an die Lebensdauer eines Objekts. Der Konstruktor erwirbt die Ressource; der Destruktor gibt sie frei. Da Destruktoren während des Stack Unwinding ausgeführt werden, bietet RAII automatisch die grundlegende Garantie.
#include <fstream>
#include <string>
#include <stdexcept>
#include <iostream>
class DatabaseConnection {
public:
explicit DatabaseConnection(const std::string& conn_str)
: conn_str_(conn_str), connected_(false) {
// Simulate connection
if (conn_str.empty()) {
throw std::invalid_argument("empty connection string");
}
connected_ = true;
std::cout << "Connected to: " << conn_str_ << '\n';
}
~DatabaseConnection() {
if (connected_) {
// Cleanup never throws
std::cout << "Disconnected from: " << conn_str_ << '\n';
connected_ = false;
}
}
void execute(const std::string& query) {
if (!connected_) throw std::runtime_error("not connected");
// Even if this function throws, the destructor
// will still close the connection during unwinding
std::cout << "Executing: " << query << '\n';
}
// Non-copyable, movable
DatabaseConnection(const DatabaseConnection&) = delete;
DatabaseConnection& operator=(const DatabaseConnection&) = delete;
DatabaseConnection(DatabaseConnection&& other) noexcept
: conn_str_(std::move(other.conn_str_)),
connected_(other.connected_) {
other.connected_ = false;
}
private:
std::string conn_str_;
bool connected_;
};
void process_data() {
DatabaseConnection db("postgres://localhost/mydb");
db.execute("SELECT * FROM users");
// If execute() throws, ~DatabaseConnection() still runs
// — the connection is always properly closed
}Copy-and-Swap für die starke Garantie
Das Copy-and-Swap-Idiom bietet die starke Exception-Garantie für Zuweisungsoperatoren. Die Idee: Erstelle zuerst eine Kopie (die werfen kann), dann tausche mit der Kopie (was nie wirft). Wenn die Kopie wirft, bleibt das Originalobjekt unberührt.
#include <algorithm>
#include <utility>
#include <cstddef>
class Matrix {
public:
Matrix(std::size_t rows, std::size_t cols)
: rows_(rows), cols_(cols),
data_(new double[rows * cols]{}) {}
// Copy constructor — may throw (allocates memory)
Matrix(const Matrix& other)
: rows_(other.rows_), cols_(other.cols_),
data_(new double[other.rows_ * other.cols_]) {
std::copy(other.data_,
other.data_ + rows_ * cols_,
data_);
}
// Move constructor — never throws
Matrix(Matrix&& other) noexcept
: rows_(other.rows_), cols_(other.cols_),
data_(other.data_) {
other.data_ = nullptr;
other.rows_ = other.cols_ = 0;
}
~Matrix() { delete[] data_; }
// No-throw swap — the key ingredient
friend void swap(Matrix& a, Matrix& b) noexcept {
using std::swap;
swap(a.rows_, b.rows_);
swap(a.cols_, b.cols_);
swap(a.data_, b.data_);
}
// Copy-and-swap assignment: STRONG guarantee
// Takes parameter by value (invokes copy/move ctor)
Matrix& operator=(Matrix other) noexcept {
swap(*this, other);
return *this;
// 'other' is destroyed here, freeing old data
}
private:
std::size_t rows_, cols_;
double* data_;
};noexcept und Move-Operationen
Die STL verlässt sich stark auf noexcept bei Move-Konstruktoren. Betrachte std::vector::push_back: Wenn der Vektor reallokieren muss, muss er Elemente vom alten Puffer in den neuen übertragen.
- Wenn der Move-Konstruktor des Elements noexcept ist, verschiebt der Vektor Elemente (schnell).
- Wenn der Move-Konstruktor werfen könnte, kopiert der Vektor stattdessen Elemente (langsam, aber sicher — wenn eine Kopie wirft, ist der Originalpuffer noch intakt).
Deshalb ist das Markieren von Move-Konstruktoren mit noexcept nicht nur gute Praxis — es kann ein 10x- oder 100x-Leistungsunterschied für Typen sein, die in Containern gespeichert werden.
Du kannst zur Kompilierzeit mit std::is_nothrow_move_constructible_v prüfen. Die STL verwendet dieses Trait intern, um ihre Strategie zu bestimmen.
Die Garantiestufe identifizieren
Beim Schreiben einer Funktion, überlege welche Garantie sie bietet. Hier ist ein Beispiel aller drei Stufen in einer einzigen Klasse.
#include <vector>
#include <string>
#include <algorithm>
#include <stdexcept>
class UserRegistry {
public:
// NO-THROW: simple getter, can never fail
std::size_t size() const noexcept {
return users_.size();
}
// STRONG GUARANTEE: either the user is added or nothing changes
void add_user(const std::string& name) {
if (name.empty()) {
throw std::invalid_argument("name cannot be empty");
}
// If push_back throws (e.g., bad_alloc during reallocation),
// vector guarantees the original data is intact.
users_.push_back(name);
}
// BASIC GUARANTEE: if an exception occurs partway through,
// some users may have been processed and some may not.
// But the object is still in a valid state (no leaks, no corruption).
void normalize_all() {
for (auto& user : users_) {
// std::transform could theoretically throw
// (e.g., if user string allocation fails during tolower)
std::transform(user.begin(), user.end(), user.begin(),
[](unsigned char c) { return std::tolower(c); });
}
}
private:
std::vector<std::string> users_;
};Befolge diese Richtlinien, um konsistent exception-sicheren Code zu schreiben.
- Verwende überall RAII: Halte niemals rohe Ressourcen — umschließe sie mit Smart Pointern oder RAII-Wrappern
- Markiere Move-Konstruktoren und swap als
noexcept: Dies ermöglicht STL-Optimierungen und bietet die No-Throw-Garantie - Erledige alle werfenden Arbeiten, bevor du den Zustand änderst: Allokiere, berechne und validiere zuerst — dann committe mit No-Throw-Operationen wie
swap - Bevorzuge das Copy-and-Swap-Idiom für Zuweisungsoperatoren, um die starke Garantie mit minimalem Aufwand zu erhalten
- Destruktoren dürfen niemals werfen: Sie sind seit C++11 implizit
noexcept
Diese Muster brechen Exception-Sicherheitsgarantien und führen zu subtilen Fehlern.
- Zustand ändern, bevor alle werfenden Operationen abgeschlossen sind: Wenn Schritt 2 von 3 wirft, hat Schritt 1 das Objekt bereits geändert
- Vergessen, dass
newstd::bad_allocwerfen kann: Jede dynamische Allokation ist ein potenzieller Wurfpunkt - Nicht-
noexceptMove-Konstruktoren: Die STL fällt stillschweigend auf Kopieren zurück und tötet die Performance - Exceptions zu breit fangen:
catch(...)ohne erneutes Werfen verbirgt das eigentliche Problem
- Die drei Garantien sind: No-Throw (gelingt immer), stark (Commit-or-Rollback) und grundlegend (gültiger, aber nicht spezifizierter Zustand)
- RAII ist das Fundament — Destruktoren laufen während des Unwinding und stellen sicher, dass Ressourcen immer freigegeben werden
- Copy-and-Swap gibt die starke Garantie: Erledige alle werfenden Arbeiten zuerst, dann
swap(wasnoexceptist) noexceptMove-Konstruktoren sind kritisch — ohne sie fälltstd::vectorbei Reallokation auf Kopieren zurück- Jede Funktion sollte mindestens die grundlegende Garantie anstreben; No-Throw wo möglich
Quiz — Teste dein Wissen
(20 XP)1. Was verspricht die starke Exception-Garantie?
2. Warum kopiert `std::vector` Elemente anstatt sie zu verschieben, wenn der Move-Konstruktor nicht `noexcept` ist?
3. Warum nimmt der Zuweisungsoperator beim Copy-and-Swap-Idiom seinen Parameter per Wert?