RAII: das grundlegende Ressourcenmuster
Lerne RAII (Resource Acquisition Is Initialization) kennen — das zentrale C++-Muster für ausnahmesichere Ressourcenverwaltung. Implementiere eigene RAII-Wrapper, verstehe lock_guard und scoped_lock und erfahre, warum RAII-Typen nur verschiebbar oder referenzgezählt sein dürfen.
RAII: im Konstruktor beschaffen, im Destruktor freigeben
RAII (Resource Acquisition Is Initialization) ist das mit Abstand wichtigste Muster in C++. Die Idee ist einfach:
1. Beschaffe eine Ressource im Konstruktor (Datei öffnen, Mutex sperren, Speicher allokieren, Datenbankverbindung aufbauen).
2. Gib die Ressource im Destruktor wieder frei (Datei schließen, Mutex entsperren, Speicher freigeben, Verbindung trennen).
Da C++ garantiert, dass Destruktoren aufgerufen werden, sobald ein Objekt seinen Gültigkeitsbereich verlässt — auch wenn eine Ausnahme geworfen wird —, macht RAII die Ressourcenverwaltung automatisch und ausnahmesicher. Du vergisst nie, freizugeben, und gibst nie doppelt frei.
Jeder gut entworfene C++-Typ, der eine Ressource verwaltet, nutzt RAII: std::string, std::vector, std::unique_ptr, std::fstream, std::lock_guard, std::unique_lock.
Einen RAII-Wrapper implementieren
Hier ist ein vollständiger RAII-Wrapper für einen Dateideskriptor (POSIX). Er beschafft die Ressource im Konstruktor, gibt sie im Destruktor frei und unterstützt Verschiebesemantik für die Besitzübertragung.
#include <unistd.h> // open, close, read, write
#include <fcntl.h> // O_RDONLY, etc.
#include <stdexcept>
#include <utility>
#include <iostream>
class FileDescriptor {
int fd_ = -1; // -1 = no resource
public:
// Acquire: open file in constructor
explicit FileDescriptor(const char* path, int flags)
: fd_(::open(path, flags)) {
if (fd_ < 0) throw std::runtime_error("Cannot open file");
}
// Release: close file in destructor
~FileDescriptor() {
if (fd_ >= 0) ::close(fd_);
}
// Non-copyable (copying a file descriptor is meaningless)
FileDescriptor(const FileDescriptor&) = delete;
FileDescriptor& operator=(const FileDescriptor&) = delete;
// Moveable (transfer ownership)
FileDescriptor(FileDescriptor&& other) noexcept
: fd_(std::exchange(other.fd_, -1)) {}
FileDescriptor& operator=(FileDescriptor&& other) noexcept {
if (this != &other) {
if (fd_ >= 0) ::close(fd_);
fd_ = std::exchange(other.fd_, -1);
}
return *this;
}
int get() const { return fd_; }
};
int main() {
try {
FileDescriptor fd("/tmp/raii_test.txt", O_RDONLY);
std::cout << "File opened: fd=" << fd.get() << '\n';
// Use fd...
} catch (const std::exception& e) {
std::cerr << e.what() << '\n';
}
// fd.~FileDescriptor() called automatically — file closed
// Even if an exception was thrown, the file is closed
}Ausnahmesicherheit durch RAII
Ohne RAII braucht ausnahmesicherer Code für jede Ressource mühsame try/catch-Blöcke. Mit RAII passiert das automatisch — jeder Destruktor wird beim Stack-Unwinding ausgeführt.
#include <memory>
#include <mutex>
#include <fstream>
#include <vector>
#include <stdexcept>
std::mutex g_mutex;
// WITHOUT RAII — manual cleanup is error-prone
void bad_example() {
std::mutex* m = &g_mutex;
m->lock();
int* data = new int[100];
// If this throws, mutex stays locked AND data leaks!
// process(data);
delete[] data;
m->unlock();
}
// WITH RAII — automatic, exception-safe cleanup
void good_example() {
std::lock_guard<std::mutex> lock(g_mutex); // RAII lock
auto data = std::make_unique<int[]>(100); // RAII allocation
// If this throws:
// 1. data's destructor frees the memory
// 2. lock's destructor unlocks the mutex
// Stack unwinding handles everything
// process(data.get());
}
// Multiple resources — RAII composes naturally
void multi_resource() {
std::lock_guard<std::mutex> lock(g_mutex);
auto buffer = std::make_unique<char[]>(4096);
std::ofstream file("/tmp/output.txt");
// If any operation throws, ALL resources are released
// in reverse order of construction:
// 1. file closed
// 2. buffer freed
// 3. mutex unlocked
}lock_guard, unique_lock & scoped_lock
Die Standardbibliothek stellt RAII-Wrapper für Mutexe bereit:
- std::lock_guard — die einfachste RAII-Sperre. Sperrt im Konstruktor, entsperrt im Destruktor. Weder verschiebbar noch kopierbar. Nimm sie, wenn du eine simple, an den Gültigkeitsbereich gebundene Sperre brauchst.
- std::unique_lock — die flexible RAII-Sperre. Sie unterstützt verzögertes Sperren, Sperren mit Zeitlimit, manuelles Ent- und Wiedersperren und lässt sich verschieben. Für die Verwendung mit std::condition_variable ist sie zwingend erforderlich.
- std::scoped_lock (C++17) — sperrt mehrere Mutexe gleichzeitig und verwendet dabei einen Algorithmus zur Deadlock-Vermeidung. Das ist der bevorzugte Sperrtyp, wenn ein oder mehrere Mutexe gesperrt werden.
std::mutex m1, m2;
// C++17: beide sperren, ohne Deadlock-Risiko
std::scoped_lock lock(m1, m2);1. Unbenannte RAII-Objekte werden sofort zerstört:
std::lock_guard<std::mutex>(mtx); // FEHLER! Temporär — sperrt und entsperrt sofort wieder
std::lock_guard<std::mutex> lock(mtx); // Richtig — benannt, lebt bis zum Ende des Gültigkeitsbereichs2. Kopiere RAII-Typen nie naiv: Ein Dateihandle oder eine Mutex-Sperre zu kopieren ergibt keinen Sinn. RAII-Typen sollten sein:
- Nur verschiebbar (wie unique_ptr, unique_lock) für exklusiven Besitz oder
- Referenzgezählt (wie shared_ptr) für geteilten Besitz.
3. Ein Destruktor darf nicht werfen: Wirft ein Destruktor während des Stack-Unwinding (also während bereits eine Ausnahme aktiv ist), wird std::terminate aufgerufen. Seit C++11 sind Destruktoren standardmäßig noexcept.
1. Jede Ressource sollte einem RAII-Objekt gehören — Speicher, Dateien, Sperren, Sockets, Datenbankverbindungen, GPU-Handles.
2. Eine Ressource pro RAII-Klasse — halte die Wrapper fokussiert.
3. Lösche die Kopieroperationen, außer das Kopieren der Ressource ist wirklich sinnvoll.
4. Implementiere Verschiebeoperationen, um die Besitzübertragung zu ermöglichen.
5. Nutze std::exchange in Verschiebeoperationen — es liest und setzt die Quelle in einem Zug zurück: fd_ = std::exchange(other.fd_, -1);
6. Bevorzuge die RAII-Typen der Standardbibliothek (unique_ptr, lock_guard, fstream) — schreibe eigene Wrapper nur für Ressourcen, für die es keinen Standardwrapper gibt.
RAII-Typen kombinieren (Rule of Zero)
Die konsequenteste Form von RAII ist die Rule of Zero: Sind alle deine Membervariablen RAII-Typen, braucht deine Klasse keine einzige benutzerdeklarierte spezielle Memberfunktion. Die vom Compiler erzeugten Destruktor-, Kopier- und Verschiebeoperationen tun automatisch das Richtige.
class DatabaseSession {
std::unique_ptr<Connection> conn_; // RAII: wird automatisch geschlossen
std::unique_lock<std::mutex> lock_; // RAII: wird automatisch entsperrt
std::vector<std::string> log_; // RAII: wird automatisch freigegeben
// Kein Destruktor, kein Kopieren, kein Verschieben nötig — die Membertypen erledigen das
};Die Rule of Zero ist das Ziel. Die Rule of Five (alle fünf speziellen Memberfunktionen explizit definieren) gilt für hardwarenahe Ressourcen-Wrapper. Ein Dazwischen gibt es nicht — deklarierst du eine der fünf, deklariere alle.
- RAII: im Konstruktor beschaffen, im Destruktor freigeben — automatisch und ausnahmesicher
- Destruktoren werden beim Stack-Unwinding aufgerufen, daher behandelt RAII Ausnahmen ganz von selbst
- RAII-Typen sollten nur verschiebbar (exklusiver Besitz) oder referenzgezählt (geteilter Besitz) sein
- Nutze
std::scoped_lock(C++17), um mehrere Mutexe deadlock-frei zu sperren - Unbenannte RAII-Objekte werden sofort zerstört — gib ihnen immer einen Namen
- Folge der Rule of Zero: Verwende RAII-Member, damit der Compiler die speziellen Memberfunktionen korrekt erzeugt
Quiz — Teste dein Wissen
(15 XP)1. Was ist an `std::lock_guard<std::mutex>(mtx);` falsch (beachte: kein Variablenname)?
2. Warum sollten RAII-Typen ihre Kopieroperationen in der Regel löschen?
3. Was besagt die Rule of Zero?