手写RAII模板类适用于std::unique_ptr等无法覆盖的资源类型,构造必须成功获取资源并失败抛异常,析构必须安全释放且绝不抛异常,拷贝需禁用、移动需置空,release/reset语义严格区分,析构执行前提为作用域清晰、生命周期可控。

手写 RAII 模板类不是为了重复造轮子,而是当 std::unique_ptr 或 std::lock_guard 无法覆盖你的资源类型时——比如 pthread_mutex_t、sem_t、mmap 区域、CUDA context 或自定义句柄——才需要它。构造必须拿资源,失败就抛异常;析构必须放资源,且绝不能抛异常。
构造函数里资源申请失败怎么办
RAII 的前提是一个“全有或全无”的对象:构造成功 → 资源归属该对象;构造失败 → 对象根本不会存在。任何“设空状态 + 后续检查”的设计都会破坏这个契约。
- 调用底层 API(如
open()、fopen()、pthread_mutex_init())后,必须立刻检查返回值;若失败(如fd == -1、fp == nullptr),直接抛std::system_error或std::runtime_error,不设默认值也不留空指针 - 禁止在构造函数里做多步初始化并容忍中间失败——比如先
malloc再setsockopt,若后者失败而前者已分配,就会泄漏 - 若底层用错误码(如 POSIX
errno),必须显式转换为异常;不能靠返回值“带病运行”,否则析构时面对无效句柄会崩溃
析构函数里释放失败能 throw 吗
不能。C++ 标准强制要求析构函数为 noexcept,一旦在栈展开过程中(即已有异常正在传播时)再抛异常,std::terminate() 立即触发,程序终止。
- 所有释放操作(
close()、fclose()、delete、munmap())必须确保不抛异常;如有失败,只记录日志或忽略(生产环境通常忽略,因资源已不可恢复) - 不要在析构中调用
std::cout、printf、malloc、网络 I/O 或任何可能抛异常的 C++ 标准库函数 - 对系统调用,检查返回值但不 throw:
if (fd_ != -1) ::close(fd_);—— 即使close()返回 -1,也绝不throw
为什么拷贝必须 delete、移动必须置空
资源(文件描述符、堆内存、互斥锁)天然不具备可复制性。允许拷贝等于允许多个对象指向同一份底层资源,析构时必然 double-free 或 double-close。
- 显式禁用:
MyRAII(const MyRAII&) = delete;和MyRAII& operator=(const MyRAII&) = delete; - 移动构造/赋值中,必须把原对象的句柄(如
fd_、ptr_)置为无效值(-1、nullptr),否则移动后原对象析构仍会释放已转出的资源 - 移动后原对象进入 “valid but empty” 状态:它仍是合法对象,析构时检查句柄有效性再决定是否释放,避免重复动作
release() 和 reset() 的语义差异
这两个函数不是可选项,而是所有权移交的关键接口,语义必须严格区分:
-
release():移交所有权,不释放资源,只把内部句柄置空并返回原始值;调用后对象不再管理该资源,也不再负责释放 -
reset(T* new_ptr):先释放当前资源(若持有),再接管新资源;若传入nullptr,则只释放不接管 - 两者都应检查当前状态(如
ptr_ != nullptr)再操作,避免空指针解引用或重复释放
最易被忽略的一点是:析构函数是否真的执行了。别依赖“写了 ~MyClass() 就一定调用”——若对象是全局或静态生命周期,析构时机不确定;若在 std::thread 中创建又未 join/detach,线程退出时对象可能被强制销毁,此时析构函数未必有机会运行。RAII 安全的前提,是对象作用域清晰、生命周期可控。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











