应优先用std::unique_ptr配noexcept自定义删除器,手写RAII模板易漏移动后置空、拷贝禁用、析构noexcept等约束;仅当资源非指针类型、需延迟初始化或强依赖运行时状态时才考虑自定义。

别从零写 RAII 模板类——std::unique_ptr 配自定义删除器已覆盖 95% 的场景,手写极易漏掉 noexcept、移动后置空、拷贝禁用等关键约束,反而引入未定义行为。
std::unique_ptr 自定义删除器必须标记为 noexcept
析构函数里调用可能失败的系统 API(如 fclose、close、CloseHandle)时,若删除器抛异常,程序会直接调用 std::terminate()。C++ 标准要求析构函数不得抛异常,而 std::unique_ptr 的移动操作依赖析构安全执行。
- 错误写法:
[](FILE* f) { fclose(f); }——fclose失败时可能设errno,但不抛异常;看似安全,实则 lambda 类型推导后默认非noexcept,某些标准库实现会在移动时拒绝调用 - 正确写法:
[](FILE* f) noexcept { if (f) fclose(f); }—— 显式声明noexcept,且加空指针检查 - 若删除器是 C 函数指针(如
free),它天然满足noexcept,可直接传入:std::unique_ptr<int void>(new int[10], free)</int>
lambda 删除器不能捕获外部变量(除非按值且 trivial)
带捕获的 lambda 类型不可默认构造、不可复制、甚至不可移动(取决于捕获内容),会导致 std::unique_ptr 编译失败,错误信息类似 “use of deleted function” 或 “no type named 'type' in struct std::invoke_result”。
- 禁止写法:
[&log](FILE* f) noexcept { log("closing"); fclose(f); }—— 引用捕获使 lambda 不可移动 - 允许写法:
[log = std::move(log)](FILE* f) noexcept { log("closing"); fclose(f); }—— 仅当log是可移动且 trivially destructible 类型才安全;否则应避免捕获,改用静态函数或全局上下文 - 更稳妥方案:把上下文封装进资源类型本身(如
struct FileWithLog { FILE* f; Logger* log; };),再写对应删除器
资源获取失败必须在构造时抛异常,不能留“半初始化对象”
RAII 的契约是“对象存在即资源就绪”。如果 fopen 返回 nullptr、open 返回 -1、malloc 返回 nullptr,就不能让对象构造成功——否则析构时面对无效句柄,fclose 或 close 会触发未定义行为。
立即学习“C++免费学习笔记(深入)”;
- 错误模式:在构造函数体里申请资源,失败只设标志位
is_valid_ = false,析构时跳过释放 —— 用户可能误调get()拿到空指针并解引用 - 正确做法:资源申请放在成员初始化列表中,并立即校验:
explicit FilePtr(FILE* f) : ptr_(f) { if (!ptr_) throw std::system_error(errno, std::generic_category(), "fopen failed"); } - 多步申请(如先
mmap再mlock)应拆成多个 RAII 对象,或用私有static工厂函数封装并确保回滚
什么时候真需要手写 RAII 模板类?
只有当 std::unique_ptr 无法满足以下任一条件时,才考虑自定义:
- 资源不是指针类型(如 Windows
HANDLE是整数,POSIXfd是int,需封装为类而非指针) - 需要延迟初始化(
std::optional<t></t>可配合自定义析构,但若需额外状态管理,模板类更可控) - 释放逻辑强依赖运行时状态(如某资源必须在特定线程销毁、或需根据当前配置决定是否 flush)
- 需统一接口管理多种句柄(
FILE*、HANDLE、int fd),此时可用模板特化 + 公共基类,而非泛型模板
即便手写,也必须显式禁用拷贝、移动后将源资源置为无效值(nullptr / -1 / INVALID_HANDLE_VALUE)、析构前判空、所有释放调用加 noexcept 声明——这些细节漏掉任意一项,都可能在特定编译器或优化级别下暴露崩溃。


















