RAII模板类必须确保构造成功即资源就绪、析构执行即资源释放:构造函数中原子申请并失败即抛异常,析构函数显式noexcept且判空释放,禁用拷贝、移动后置空,仅在标准智能指针无法满足非堆资源或上下文敏感释放时自研。

手写 RAII 模板类不是“加个析构函数就行”,而是必须让对象存在即资源就绪、对象销毁即资源释放——否则就是裸指针换了个马甲,照样 double-free 或泄漏。
构造函数里必须完成资源申请,失败就 throw
RAII 的契约是:对象构造成功 = 资源已拿到;构造失败 = 对象根本不存在。如果在构造函数体里做 new 或 fopen,中间抛异常,成员变量可能部分初始化,析构函数不调用,资源就漏了。
- 资源申请必须放在成员初始化列表中,或至少在构造函数最开头原子执行
- 失败时直接
throw std::system_error(errno, std::generic_category()),别设is_valid_标志位“带病上岗” - POSIX API(如
open())返回 -1、fopen()返回nullptr,必须显式检查并转为异常 - 多步申请(比如先
mmap再mlock)不要塞进一个构造函数,拆成多个 RAII 对象,或用static工厂函数封装回滚逻辑
析构函数必须 noexcept 且只做安全释放
~MyRAII() 里调 close() 或 delete 失败再 throw,程序会直接 std::terminate()——尤其当另一个异常正在传播时,这是 C++ 的硬性限制。
- 析构函数必须显式声明为
noexcept,不能依赖默认行为 - 释放前必判空:
if (fd_ != -1) ::close(fd_);,无效句柄(如INVALID_HANDLE_VALUE、-1)不能传给释放函数 - 忽略系统调用返回值是标准做法;若需调试感知失败,仅限
assert()或写日志,生产环境不阻塞、不重试 - 禁止在析构中调用
std::cout、malloc、网络 I/O 等非异步信号安全函数
拷贝必须 delete,移动后原对象必须置空
允许拷贝等于给 double free 或重复 close 埋雷;移动后不置空,原对象析构时仍会释放已被转移的资源。
立即学习“C++免费学习笔记(深入)”;
- 显式删除:
MyRAII(const MyRAII&) = delete;和MyRAII& operator=(const MyRAII&) = delete; - 移动构造中,必须将源对象句柄设为无效值:
other.fd_ = -1;或other.ptr_ = nullptr; - 移动后原对象要保持 “valid but empty” 状态,析构时仍需检查有效性,避免二次释放
- 别写
= default——编译器生成的是浅拷贝,等同于没禁用
什么时候该自己写,而不是用 std::unique_ptr
std::unique_ptr 已覆盖绝大多数堆内存场景,手写 RAII 模板只在标准库无法满足时才值得投入。
- 你要管理非堆资源:文件描述符(
int fd)、WindowsHANDLE、pthread_mutex_t、OpenGL texture ID - 释放逻辑带上下文:比如 OpenSSL 的
EVP_CIPHER_CTX需先EVP_CIPHER_CTX_cleanup()再OPENSSL_free() - 需要定制释放前动作:如文件句柄要先
fflush()再fclose(),或释放前广播事件、打日志 - 资源类型本身不可复制也不可移动(如某些 legacy C 结构),需设计为仅栈上短生命周期使用
最容易被忽略的点是:自定义 deleter 若捕获局部变量(如用带捕获的 lambda),就无法作为模板参数传入 std::unique_ptr;而手写模板若在析构里调用可能抛异常的函数(哪怕只是 std::cout),就踩中 noexcept 红线——这种错误不会编译报错,但会在异常传播路径上静默杀死进程。


















