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í.
#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.
#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);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 end2. 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+.
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.
- 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žijte
std::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?