raii核心是资源与对象生命周期严格绑定,禁止拷贝、按需移动;std::unique_ptr仅默认管理堆内存,而raii适用于文件描述符、gpu buffer等任意资源,需自定义删除器及异常安全的构造/析构逻辑。

RAII 不是“加个析构函数就行”,关键在于资源生命周期必须与对象生命周期严格绑定,且禁止拷贝、允许移动(或按需禁用移动)——否则资源重复释放或悬空指针立刻出现。
为什么 std::unique_ptr 不能直接替代自定义 RAII 类
内置智能指针只管理堆内存,而 RAII 的核心是“任意资源”:文件描述符、GPU buffer、Windows HANDLE、数据库连接、信号量……这些资源没有统一的 delete 语义,必须显式调用 close()、vkDestroyBuffer()、ReleaseMutex() 等。
-
std::unique_ptr的默认删除器是delete,对int fd或HANDLE h完全无效 - 可传自定义删除器,但每次都要写 lambda 或函数对象,模板参数冗长,易出错
- 无法内聚资源创建逻辑(如
open()失败时抛异常或返回错误码的处理)
构造函数必须完成资源申请,失败则不构造成功
RAII 要求“要么完全构造成功,要么根本没对象”——这靠构造函数异常安全保证。若 open("/dev/tty", O_RDWR) 失败,绝不能留下一个 fd = -1 的半成品对象。
- 构造函数内做资源申请,失败立即
throw(如std::system_error) - 禁止在构造函数里用
if (fail) return;—— 这会静默构造出无效对象 - 成员变量只能是“无状态”的(如
int fd_ = -1;),资源句柄不能靠默认初始化“占位”
class FileHandle {
int fd_;
public:
explicit FileHandle(const char* path) : fd_(::open(path, O_RDONLY)) {
if (fd_ == -1) {
throw std::system_error{errno, std::generic_category(), "open failed"};
}
}
// ...
};
析构函数必须无异常、无条件释放资源
析构函数是 RAII 的最终防线,它不能失败,也不能因异常中断释放流程(否则资源泄漏)。C++11 起应显式标注 noexcept。
- 释放操作本身可能失败(如
close()返回 -1),但此时只能记录日志或忽略——绝不能抛异常 - 避免在析构中调用可能抛异常的函数(如
std::string构造、容器操作) - 若资源释放有依赖顺序(如先 flush 再 close),必须在析构中严格编码,不可依赖成员销毁顺序
~FileHandle() noexcept {
if (fd_ != -1) {
::close(fd_); // 忽略返回值;即使失败也绝不 throw
}
}
拷贝和移动必须按语义明确禁止或转移
资源本质是独占的。允许拷贝等于两个对象指向同一文件描述符,析构时 double-close;允许默认移动则可能把已关闭的 fd_ = -1 移走,留下原对象误用。
- 禁用拷贝:
FileHandle(const FileHandle&) = delete;和operator=(const FileHandle&) = delete; - 显式实现移动:将源对象资源置为无效态(如
fd_ = -1),再转移句柄值 - 移动构造/赋值都必须是
noexcept,否则容器(如std::vector)扩容时可能放弃移动语义
FileHandle(FileHandle&& other) noexcept : fd_(other.fd_) {
other.fd_ = -1;
}
FileHandle& operator=(FileHandle&& other) noexcept {
if (this != &other) {
if (fd_ != -1) ::close(fd_);
fd_ = other.fd_;
other.fd_ = -1;
}
return *this;
}
最易被忽略的一点:资源释放函数本身是否线程安全?比如 pthread_mutex_destroy() 要求互斥量未被任何线程持有;vkDestroyBuffer() 要求 buffer 不再被任何提交的 command buffer 引用。RAII 模板无法自动检查这些前置条件——它只保证“调用了”,不保证“能成功”。这类约束必须由使用者在更高层控制。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











