RAII模板类核心是构造时获取资源并失败即抛异常、析构时noexcept释放、禁用拷贝、强制移动置空、支持自定义删除器,仅在std::unique_ptr不适用(如封装mutex、mmap、CUDA context)时才自研。

自己写 RAII 模板类不是为了替代 std::unique_ptr,而是当标准库不适用时(比如封装 pthread_mutex_t、mmap 区域、CUDA context 或自定义系统句柄)才需要动手——否则直接用 std::unique_ptr 更安全、更少出错。
构造函数必须成功获取资源,失败就抛异常
RAII 的核心是“对象存在 ⇔ 资源已就绪”。如果 open() 返回 -1、malloc() 返回 nullptr、pthread_mutex_init() 失败,构造函数就不能让对象半途而废地活下来。
- 不要在构造函数里设“空状态”字段(如
ptr_ = nullptr)然后静默容忍失败;那会让析构函数面对无效资源,容易触发double free或空指针解引用 - 底层 API 返回错误码(如 POSIX 的
errno)时,必须显式检查并转为异常,例如throw std::system_error(errno, std::generic_category()) - 避免在构造中做可失败但不抛异常的操作,比如只检查文件是否存在却不打开——这不属于 RAII 职责,是业务逻辑
- 构造函数声明为
explicit,防止隐式转换导致意外资源申请
析构函数必须 noexcept 且只做释放
析构函数一旦抛异常,尤其在栈展开过程中,会直接调用 std::terminate()。这不是 bug,是 C++ 的硬性约束。
- 所有释放操作(
close()、delete、munmap()、pthread_mutex_destroy())必须确保不会抛异常;如有失败,记录日志或忽略,绝不throw - 析构函数必须声明为
noexcept,这是 RAII 类异常安全的底线 - 禁止在析构中调用
printf、std::cout、malloc、网络 I/O 或任何可能阻塞/抛异常的函数 - 若资源释放有依赖顺序(如先
fflush()再fclose()),必须在析构中显式编码,不能依赖成员变量销毁顺序
拷贝必须禁用,移动必须置空原资源
资源通常不可复制:一个 socket、一个 mutex、一块 mmap 区域,无法被两个对象同时持有。允许拷贝等于主动制造 double free 或竞态。
- 显式删除拷贝构造和拷贝赋值:
MyRAII(const MyRAII&) = delete;、MyRAII& operator=(const MyRAII&) = delete; - 移动构造/赋值中,必须将源对象的资源句柄置为无效值(如指针设为
nullptr、fd 设为-1) - 移动后原对象仍需可安全析构——析构前要检查句柄是否有效,否则会重复释放
- 移动语义不是可选项;若类要从工厂函数返回或作为函数返回值,没移动支持就编译不过
支持自定义释放策略,而非硬编码 delete
RAII 的价值远不止管理堆内存。new/delete 只是特例,真正难的是封装 fopen/fclose、shm_open/shm_unlink、Windows CreateFile/CloseHandle 等。
- 模板参数应接受释放器(
Deleter),类型可以是函数指针、lambda 或仿函数 - 释放器必须满足:接收资源类型参数、不抛异常、可
noexcept调用 - 避免裸指针外泄:资源句柄必须
private,只提供get()(返回const)、release()(移交所有权)、operator->()(带断言的访问)等受控接口 - 不要提供返回非
const裸指针的get(),否则用户手动close()就绕过了 RAII
最常被忽略的一点是:析构函数里调用 close() 或 fclose() 失败时,很多人下意识想重试或报错退出——但 RAII 不负责恢复,只负责“这次一定释放”。失败了就记日志,然后继续;程序该崩溃时不会因这个失败立刻崩,但若在这里 throw,它一定会崩得毫无商量余地。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











