Přeskočit na obsah

RAII: Vzor základního zdroje

Learn RAII (Resource Acquisition Is Initialization) — the core C++ pattern for exception-safe resource management. Implement RAII wrappers, understand lock_guard and scoped_lock, and learn why RAII types must be move-only or ref-counted.

RAII: Získání v konstruktoru, uvolnění v destruktoru

RAII (Resource Acquisition Is Initialization) je nejdůležitějším vzorem v C++. Myšlenka je jednoduchá:

1. Získejte prostředek v konstruktoru (otevřete soubor, uzamknete mutex, přidělíte paměť, připojíte se k databázi).
2. Uvolněte prostředek v destruktoru (zavřete soubor, odemkněte mutex, uvolněte paměť, odpojte).

Protože C++ zaručuje, že destruktory jsou volány, když se objekt dostane mimo rozsah – i když je vyvolána výjimka – RAII činí správu prostředků automatickou a bezpečnou pro výjimky. Nikdy nezapomeneš uvolnit a nikdy neuvolníš dvakrát.

Každý dobře navržený typ C++, který spravuje zdroj, používá RAII:std::string,std::vector,std::unique_ptr,std::fstream,std::lock_guard,std::unique_lock.

Implementace RAII Wrapperu

Zde je kompletní RAII obal pro deskriptor souboru (POSIX). Získává v konstruktoru, uvolňuje v destruktoru a podporuje sémantiku pohybu pro převod vlastnictví.

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
}

Výjimečná bezpečnost přes RAII

Bez RAII vyžaduje kód bezpečný pro výjimky zdlouhavé bloky pokusů/úlovků pro každý zdroj. S RAII je to automatické – každý destruktor se spustí během odvíjení zásobníku.

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

Standardní knihovna poskytuje obaly RAII pro mutexy:

- std::lock_guard — nejjednodušší RAII zámek. Uzamyká v konstruktoru, odemyká v destruktoru. Nepřenosné, nekopírovatelné. Použijte, když potřebujete jednoduchý zámek s rozsahem.

- std::unique_lock — flexibilní RAII zámek. Podporuje odložené zamykání, časované zamykání, ruční odemykání / opětovné zamykání a lze jej přesunout. Vyžaduje se pro použití s ​​std::condition_variable.

- std::scoped_lock (C++17) – uzamkne více mutexů současně pomocí algoritmu pro zamezení uváznutí. Toto je preferovaný typ zámku při zamykání jednoho nebo více mutexů.

std::mutex m1, m2;
// C++17: lock both without deadlock risk
std::scoped_lock lock(m1, m2);
Úskalí

1. Nepojmenované objekty RAII jsou okamžitě zničeny:

std::lock_guard<std::mutex>(mtx); // BUG! Temporary — locks and immediately unlocks
std::lock_guard<std::mutex> lock(mtx); // Correct — named, lives until scope end

2. Never naively copy RAII types: Copying a file handle or mutex lock makes no sense. RAII types should be:
- Move-only (like unique_ptr, unique_lock) for exclusive ownership, or
- Reference-counted (like shared_ptr) for shared ownership.

3. Destructor should not throw: If a destructor throws during stack unwinding (another exception is active), std::terminate is called. Destructors are noexcept ve výchozím nastavení v C++11+.

Osvědčený postup

1. Každý prostředek by měl být vlastněn objektem RAII – paměť, soubory, zámky, sokety, databázová připojení, ovladače GPU.
2. Jeden zdroj na třídu RAII — udržujte obálky zaměřené.
3. Smažte operace kopírování, pokud kopírování zdroje nemá smysl.
4. Implementujte operace přesunu, abyste umožnili převod vlastnictví.
5. Použijtestd::exchangev operacích přesunu — atomicky čte a resetuje zdroj:fd_ = std::exchange(other.fd_, -1);
6. Upřednostňujte standardní typy RAII (unique_ptr,lock_guard,fstream) — pište pouze vlastní obaly pro zdroje bez standardních obalů.

Skládání typů RAII (pravidlo nuly)

Konečným vyjádřením RAII je Pravidlo nuly: pokud jsou všechny vaše členské proměnné typu RAII, vaše třída nepotřebuje žádné uživatelem deklarované speciální členy. Kompilátorem generovaný destruktor, operace kopírování a přesunu dělají správnou věc automaticky.

class DatabaseSession {
    std::unique_ptr<Connection> conn_;    // RAII: auto-closed
    std::unique_lock<std::mutex> lock_;   // RAII: auto-unlocked
    std::vector<std::string> log_;        // RAII: auto-freed
    // No destructor, copy, or move needed — all member types handle it
};

Cílem je pravidlo nuly. Pravidlo pěti (explicitně definuje všech 5 speciálních členů) je pro nízkoúrovňové typy obalů prostředků. Neexistuje žádná střední cesta – pokud deklarujete kteroukoli z pěti, deklarujte všechny.

Klíčové poznatky
  • RAII: získání v konstruktoru, uvolnění v destruktoru – automatické a bezpečné pro výjimky
  • Destruktory jsou volány během odvíjení zásobníku, takže RAII zpracovává výjimky přirozeně
  • Typy RAII by měly být pouze pro přesun (výhradní vlastnictví) nebo počítané s referencí (sdílené vlastnictví)
  • Použijtestd::scoped_lock(C++17) pro zamykání více mutexů bez uváznutí
  • Nepojmenované objekty RAII jsou okamžitě zničeny – vždy jim dejte jméno
  • Dodržujte pravidlo nuly: použijte členy RAII, aby kompilátor vygeneroval správné speciální členy

Kvíz — Otestujte své znalosti

(15 XP)

1. Co je špatného na`std::lock_guard<std::mutex>(mtx);`(poznámka: žádný název proměnné)?

2. Proč by typy RAII měly obvykle odstraňovat své operace kopírování?

3. Co je pravidlo nuly?