RAII: модель фундаментальных ресурсов
Изучите RAII (Resource Acquisition Is Initialization) — базовый шаблон C++ для управления ресурсами, безопасными для исключений. Внедрите обертки RAII, поймите lock guard и scoped lock и узнайте, почему типы RAII должны быть только для перемещения или пересчитаны.
RAII: Приобретение в Constructor, выпуск в Destructor
RAII (Resource Acquisition Is Initialization) является единственным наиболее важным шаблоном в C++. Идея проста:
1. Приобрести ресурс в конструкторе** (открыть файл, заблокировать mutex, выделить память, подключиться к базе данных).
2. Выпустить ресурс в деструкторе (закрыть файл, разблокировать mutex, свободную память, отключить).
Поскольку C++ гарантирует, что деструкторы вызываются, когда объект выходит из-под контроля — даже если исключение забрасывается — RAII делает управление ресурсами автоматическим и безопасным. Вы никогда не забываете выпустить, и вы никогда не выпускаете дважды.
Каждый хорошо разработанный тип C++, который управляет ресурсом, использует RAII: std::string, std::vector, std::unique_ptr, std::fstream, std::lock_guard, std::unique_lock.
Внедрение RAII Wrapper
Вот полная обертка RAII для дескриптора файлов (POSIX). Он приобретает в конструкторе, выпускает в деструкторе и поддерживает семантику перемещения для передачи права собственности.
#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
}Безопасность через RAII
Без RAII безопасный код требует утомительных блоков для каждого ресурса. С RAII он автоматический — каждый деструктор стреляет во время раскручивания стопки.
#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, уникальный lock & scoped lock
Стандартная библиотека предоставляет обертки RAII для мутексов:
std::lock_guard — самый простой замок RAII. Замки в конструкторе, разблокировки в деструкторе. Неподвижный, некопируемый. Используйте, когда вам нужен простой замок.
std::unique_lock — гибкий замок RAII. Поддерживает отложенную блокировку, временную блокировку, ручную разблокировку / блокировку и может перемещаться. Требуется для использования с std::condition_variable.
std::scoped_lock (C++17) — блокирует несколько мутексов одновременно с использованием алгоритма избежания тупика. Это предпочтительный тип замка при блокировке одной или нескольких мутексов.
std::mutex m1, m2;
// C++17: lock both without deadlock risk
std::scoped_lock lock(m1, m2);1 Неназванные объекты RAII немедленно уничтожаются:
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. Никогда наивно не копируйте типы RAII: Копирование ручки файла или блокировки mutex не имеет смысла. Типы RAII должны быть:
Только для движения (например, unique_ptr, unique_lock) для исключительного владения, или
Справочная информация (например, shared_ptr) для долевого владения.
3. Деструктор не должен бросать: Если деструктор бросает во время раскручивания стека (активно другое исключение), называется std::terminate. Деструкторы noexcept по умолчанию в C++11+.
1. Каждый ресурс должен принадлежать объекту RAII — памяти, файлам, замкам, розеткам, соединениям с базами данных, GPU-ручкам.
2. Один ресурс на класс RAII — держите обертки сфокусированными.
3. Удалить операции по копированию, если только копирование ресурса не имеет смысла.
4. Внедрить операции по перемещению для обеспечения передачи прав собственности.
5. Использовать std::exchange в операциях перемещения — он атомарно считывает и сбрасывает источник: fd_ = std::exchange(other.fd_, -1);
6. Предпочтительно стандартные типы RAII (unique_ptr, lock_guard, fstream) — пишут только пользовательские обертки для ресурсов без стандартных оберток.
Типы RAII (правило нуля)
Окончательным выражением RAII является правило нуля: если все переменные членов являются типами RAII, ваш класс не нуждается в специальных членах, объявленных пользователем. Сгенерированный компилятором деструктор, копирование и перемещение операций делают правильные вещи автоматически.
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
};Правило нуля – это цель. Правило пяти (явно определяет всех 5 специальных членов) относится к низкоуровневым типам обертки ресурсов. Нет середины — если вы объявите кого-то из пяти, объявите всех.
- RAII: приобретение в конструкторе, выпуск в деструкторе — автоматический и безопасный
- Деструкторы называются во время раскручивания стека, поэтому RAII обрабатывает исключения естественным образом.
- Типы RAII должны быть только для перемещения (эксклюзивное владение) или учитываться (совместное владение)
- Используйте
std::scoped_lock(C++17) для блокировки мультимутекса без тупика - Неназванные объекты RAII уничтожаются сразу — всегда дайте им имя
- Следуйте правилу нуля: используйте RAII, чтобы компилятор генерировал правильные специальные члены.
Тест — проверьте свои знания
(15 XP)1. Что не так с `std::lock_guard<std::mutex>(mtx);` (примечание: нет переменного имени)?
2. Почему RAII-типы обычно удаляют свои операции с копиями?
3. Что такое правило нуля?