Zum Inhalt springen

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.

raii_file_descriptor.cpp
#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.

exception_safety.cpp
#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);
Stolperfalle

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ültigkeitsbereichs

2. 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.

Best Practice

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.

Wichtigste Erkenntnisse
  • 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?