RAII: Modelul de resurse fundamentale
Aflați RAII (Resource Acquisition Is Initialization) — modelul de bază C++ pentru gestionarea resurselor în condiții de siguranță pentru excepții. Implementați pachetele RAII, înțelegeți lock_guard și scoped_lock și aflați de ce tipurile RAII trebuie să fie doar pentru mișcare sau ref-contorizate.
RAII: Achiziție în Constructor, Lansare în Destructor
RAII (Resource Acquisition Is Initialization) este cel mai important model din C++. Ideea este simpla:
1. Achiziționați o resursă în constructor (deschideți un fișier, blocați un mutex, alocați memorie, conectați-vă la o bază de date).
2. Eliberați resursa în destructor (închideți fișierul, deblocați mutex-ul, eliberați memoria, deconectați).
Deoarece C++ garantează că destructorii sunt apelați atunci când un obiect iese din domeniul de aplicare - chiar dacă este aruncată o excepție - RAII face gestionarea resurselor automată și sigură pentru excepții. Nu uiți niciodată să eliberezi și nu eliberezi niciodată de două ori.
Fiecare tip C++ bine proiectat care gestionează o resursă utilizează RAII:std::string,std::vector,std::unique_ptr,std::fstream,std::lock_guard,std::unique_lock.
Implementarea unui Wrapper RAII
Iată un wrapper complet RAII pentru un descriptor de fișier (POSIX). Acesta dobândește în constructor, eliberează în destructor și acceptă semantica de mutare pentru transferul proprietății.
#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
}Excepție Siguranță prin RAII
Fără RAII, codul sigur pentru excepții necesită blocuri de încercare/captură plictisitoare pentru fiecare resursă. Cu RAII, este automat - fiecare destructor se declanșează în timpul derulării stivei.
#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 și scoped_lock
Biblioteca standard oferă pachete RAII pentru mutexuri:
- std::lock_guard — cea mai simplă blocare RAII. Se blochează în constructor, se deblochează în destructor. Nedeplasabil, necopiabil. Utilizați atunci când aveți nevoie de o încuietoare simplă.
- std::unique_lock — încuietoare flexibilă RAII. Acceptă blocarea amânată, blocarea temporizată, deblocarea/reblocarea manuală și poate fi mutată. Necesar pentru utilizare custd::condition_variable.
- std::scoped_lock (C++17) — blochează mai multe mutexuri simultan, folosind un algoritm de evitare a blocajului. Acesta este tipul de blocare preferat atunci când blocați unul sau mai multe mutexuri.
std::mutex m1, m2;
// C++17: lock both without deadlock risk
std::scoped_lock lock(m1, m2);1. Obiectele RAII fără nume sunt distruse imediat:
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 implicit în C++11+.
1. Fiecare resursă ar trebui să fie deținută de un obiect RAII — memorie, fișiere, lacăte, socluri, conexiuni la baze de date, mânere GPU.
2. O resursă per clasă RAII — menține împachetările concentrate.
3. Ștergeți operațiunile de copiere cu excepția cazului în care copierea resursei este semnificativă.
4. Implementați operațiuni de mutare pentru a activa transferul de proprietate.
5. Utilizațistd::exchangeîn operațiunile de mutare — citește și reseta atomic sursa:fd_ = std::exchange(other.fd_, -1);
6. Preferați tipurile standard RAII (unique_ptr,lock_guard,fstream) — scrieți numai wrapper-uri personalizate pentru resurse fără wrapper-uri standard.
Compunerea tipurilor RAII (regula lui zero)
Expresia supremă a RAII este Regula lui Zero: dacă toate variabilele membre sunt tipuri RAII, clasa dumneavoastră nu are nevoie de nu membri speciali declarați de utilizator. Operațiile de distrugere, copiere și mutare generate de compilator fac ceea ce trebuie în mod automat.
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
};Regula lui Zero este scopul. Regula celor cinci (definiți în mod explicit toți cei 5 membri speciali) este pentru tipurile de pachete de resurse de nivel scăzut. Nu există cale de mijloc - dacă declari oricare dintre cele cinci, declară-le pe toate.
- RAII: achiziționare în constructor, eliberare în destructor - automat și în siguranță pentru excepții
- Destructorii sunt apelați în timpul derulării stivei, astfel încât RAII gestionează excepțiile în mod natural
- Tipurile RAII ar trebui să fie numai mutare (proprietate exclusivă) sau numărate în funcție de referință (proprietate comună)
- Utilizați
std::scoped_lock(C++17) pentru blocarea multi-mutex fără blocaje - Obiectele RAII fără nume sunt distruse imediat - dați-le întotdeauna un nume
- Urmați regula zero: utilizați membri RAII astfel încât compilatorul să genereze membri speciali corecti
Test — Verifică-ți cunoștințele
(15 XP)1. Ce este în neregulă cu`std::lock_guard<std::mutex>(mtx);`(notă: fără nume de variabilă)?
2. De ce ar trebui să-și ștergă tipurile RAII de obicei operațiunile de copiere?
3. Ce este Regula lui Zero?