Item 13:以对象管理资源
对齐 Effective C++ 第三版 Item 13:资源获取后立即交给 RAII 对象,以 destructor 覆盖正常和异常退出,并选择 value、unique、shared、weak 或 custom ownership。
学习目标
- 能复现手工 new/delete、open/close 与 lock/unlock 在 early return 和 exception 下的泄漏路径
- 能实现 acquisition-is-initialization 的 RAII owner,使资源获取后立即受对象管理并在所有退出路径释放一次
- 能比较 value、unique、shared、weak 与 custom-deleter ownership,设计 factory 和失败注入测试
资源所有权实验
先预测:哪个退出路径会泄漏?
先判断 acquire 后是否立即进入 owner,再切换 RAII 与 smart pointer 策略查看证据。
观察
raw acquire 后若先执行可能 return/throw 的业务语句,释放责任就藏在控制流里;early return、exception 和新增分支都可能制造 resource leak。
决策
资源成功后立即构造 owner,把 release 绑定到对象生命周期;业务函数只接收借用引用,不把所有权留在注释里。
当前场景 · objects to manage resources
资源成功后立即构造 owner,把 release 绑定到对象生命周期;业务函数只接收借用引用,不把所有权留在注释里。
从“最后一行 delete 为什么不可靠”开始
void processInvestment()
{
Investment* investment = createInvestment();
performAudit(*investment); // 可能抛异常
if (!investment->isValid())
return; // 提前返回
delete investment;
}只要函数在 delete 前 return、throw 或新增分支,资源就泄漏。
↡程序取得某项资源后承担在恰当时机执行配对释放动作的责任。手工协议把责任藏在控制流里;分支越多,证明越困难。
Item 13 的原则是 Use objects to manage resources(以对象管理资源):把释放责任交给具有确定 destructor 的 owner,而不是交给每条控制流路径记忆。
↡申请的内存、句柄、锁或事务状态在不再需要后未被归还或结束。不只指 heap memory;锁泄漏可变成 deadlock,事务泄漏可占用连接。
RAII 的两条核心原则
↡Resource Acquisition Is Initialization,把资源获取成功与管理对象完成初始化绑定的习惯。原书 Item 13 有两条关键要求:获得资源后立刻放进管理对象;管理对象用 destructor 确保释放。
↡资源创建表达式成功后,不经过可能 throw/return 的业务语句,立即建立唯一 owner。void processInvestment()
{
auto investment = createInvestment(); // 返回 unique_ptr
performAudit(*investment);
if (!investment->isValid())
return;
}正常 return、异常、break 都沿同一对象生命周期清理。
factory 应直接返回 owner
旧接口返回 raw pointer:
Investment* createInvestment();调用方必须猜测是否拥有、用 delete 还是特定函数释放。现代接口直接表达:
std::unique_ptr<Investment> createInvestment();若实现失败,局部资源自动回收;成功后 owner 一次 move 给调用方。
↡不负责延长或结束资源生命周期,只在 owner 保证存活期间访问的引用、指针或 view。业务函数通常接收 Investment&/const Investment&,而不是复制 owner。
unique ownership 是默认起点
↡同一时刻只有一个 owner 负责资源释放,所有权可 move 但不可 copy 的语义。using InvestmentPtr = std::unique_ptr<Investment>;
InvestmentPtr createInvestment(Kind kind)
{
if (kind == Kind::Stock)
return std::make_unique<Stock>();
return std::make_unique<Bond>();
}开销通常接近 raw pointer(无状态 deleter 时),并通过类型禁止意外复制。
shared ownership 有额外语义
↡多个 owner 通过引用计数共同延长资源生命周期,最后一个 owner 销毁时释放。std::shared_ptr<const Texture> texture = loadSharedTexture(key);shared_ptr 涉及原子计数、control block、潜在循环和不可预测释放线程。它表达业务共享,不只是防 leak 工具。
↡不增加 shared owner 计数、可尝试 lock 获得临时 shared owner 的观察句柄。用于打破 ownership cycle 或缓存观察。
原书 auto_ptr 的历史位置
↡早期标准独占智能指针,以复制语法转移所有权,已在现代 C++ 删除。原书使用 auto_ptr 说明“对象管理资源”,但其 copy 会让源变 null,无法满足正常值语义,也不能安全放入标准容器。现代对应方案是 unique_ptr 与 move。
auto first = std::make_unique<Investment>();
auto second = std::move(first);
assert(!first);比 auto_ptr 的隐式“复制即转移”更清晰。
非内存资源也用对象管理
↡通过自定义 deleter 把非 new 资源句柄和配对释放函数绑定的 owner。struct FileCloser {
void operator()(std::FILE* file) const noexcept
{
if (file)
std::fclose(file);
}
};
using FilePtr = std::unique_ptr<std::FILE, FileCloser>;
FilePtr openFile(const char* path)
{
FilePtr file{std::fopen(path, "rb")};
if (!file)
throw std::system_error(errno, std::generic_category());
return file;
}不要用 delete 释放 malloc/fopen/系统句柄。
锁与作用域状态
↡构造时取得 mutex,析构时解锁的专用 RAII guard。void updateSharedState()
{
std::lock_guard lock{mutex};
mutate();
validate(); // 抛异常时仍解锁
}RAII 的本质是作用域绑定,不要求资源一定是 pointer。
构造失败时已完成成员自动回滚
↡对象构造中途抛异常时,已完成构造的基类和成员按逆序析构的语言保证。class Session {
public:
Session()
: socket_{openSocket()},
buffer_{allocateBuffer()},
registration_{registerSession()} {}
private:
SocketOwner socket_;
BufferOwner buffer_;
RegistrationOwner registration_;
};若 registration 构造失败,buffer/socket owner 自动析构。Session destructor 不会运行,因为完整 Session 未构造。
↡多个 RAII 成员按声明顺序获取、逆序释放,使构造失败自动撤销已完成步骤的模式。比手写 try/catch 清理列表更可靠。
destructor 仍必须不抛
↡管理对象析构时执行的释放动作保证异常不越过析构边界。Item 8 的原则直接适用。若 close/commit 失败需要调用方处理,提供显式接口;destructor 只做 no-throw fallback。
class Transaction {
public:
void commit();
~Transaction() noexcept { rollback_if_needed_noexcept(); }
};不能因 RAII 就把所有动作塞进 destructor。
避免 owner 与 borrower 混淆
↡接口从签名即可看出参数是取得所有权、共享所有权还是仅借用。void consume(std::unique_ptr<Job> job); // 取得 unique ownership
void retain(std::shared_ptr<Job> job); // 共享 ownership
void inspect(const Job& job); // borrow
void maybeInspect(const Job* job); // nullable borrow不要所有参数都写 shared_ptr 或 raw pointer 再靠注释解释。
↡把 owner 转成裸引用/指针供短期访问,但不改变所有权的操作。*ptr/ptr.get() 只应在 owner 保证存活的范围内使用;不能把 borrow 保存得比 owner 更久。
循环 shared ownership
↡多个 shared owner 相互持有形成闭环,引用计数永不归零的泄漏。struct Node {
std::vector<std::shared_ptr<Node>> children;
std::weak_ptr<Node> parent;
};父拥有子,子只观察父,明确方向。若图语义无法定义 owner,考虑 arena/graph owner 与 raw/weak handles,而非任意 shared。
↡由单一 graph/arena 对象拥有全部节点,节点间只保存非拥有 handle 的模型。常比复杂 shared 环更容易证明生命周期。
失败注入证明所有路径释放一次
↡记录资源 create/acquire 与 destroy/release 次数和身份的测试工具。先预测:在 Session 第二、第三个成员构造时抛异常,哪些 owner destructor 执行?完整构造后业务函数抛异常又如何?
- 每个成功 acquire 最终恰好一个 matching release。
- 第 N 个成员构造失败时此前资源逆序释放。
- early return、exception、cancel 都无需手写 cleanup。
- unique owner move 后源为空,目标唯一释放。
- shared/weak cycle 测试中最后 owner 释放后资源计数归零。
- deleter 故障不逃离 destructor,并按 Item 8 策略记录。
小结
- 手工释放依赖每条控制路径,early return 与 exception 容易泄漏
- 资源获取后立即交给管理对象,destructor 统一正常和异常退出
- factory 应直接返回 owner,业务函数通常只接收 borrow
- 默认选择 value 或 unique ownership,确有共同生命周期才使用 shared ownership
- custom deleter、lock_guard 和 scope guard 把非内存资源也纳入 RAII
- 失败注入与 resource ledger 证明每个成功获取都有且只有一次释放
名词解释
本章出现的专业名词,用大白话再讲一遍。
- resource ownership
取得资源后承担配对释放的责任。
- resource leak
资源不再需要后未被归还或结束。
- RAII
把资源获取与管理对象初始化绑定的习惯。
- immediate ownership transfer
获取成功后立即建立 owner 的规则。
- deterministic cleanup
作用域结束或栈展开时自动执行的释放。
- owning factory
创建资源并返回已建立 owner 的函数。
- borrowed access
不延长或结束生命周期的短期资源访问。
- unique ownership
同一时刻只有一个可 move owner 的语义。
- unique_ptr
以 deleter 管理独占 pointer 资源的标准类型。
- control block
保存 shared/weak 计数与 deleter 的结构。
- weak_ptr
不增加 owner 计数的 shared resource 观察句柄。
- auto_ptr
以复制语法转移所有权、已删除的旧智能指针。
- ownership move
显式把 unique owner 从源转给目标。
- custom-deleter owner
将非 new 句柄与配对释放函数绑定的 owner。
- paired release protocol
资源必须使用对应 API 家族释放的约定。
- lock_guard
构造加锁、析构解锁的 RAII guard。
- scope guard
离开作用域时执行注册清理动作的对象。
- partial-construction cleanup
构造失败时已完成子对象自动逆序析构。
- memberwise rollback
RAII 成员按获取逆序自动撤销构造步骤。
- no-throw release
释放动作不让异常越过析构边界的保证。
- explicit resource commit
需要调用方显式观察的可失败提交。
- ownership-signaling API
签名直接表达取得、共享或借用责任的接口。
- borrow projection
从 owner 产生短期裸引用但不转移所有权。
- ownership cycle
shared owner 相互持有导致计数不归零的环。
- aggregate owner
单一图或 arena 对象集中拥有全部节点的模型。
- resource ledger
记录 acquire/release 身份与次数的测试工具。
练习
- 问题 1:objects to manage resources 与 raii。 函数依次 new 对象、打开文件、加锁,中途多处 return/throw,设计 RAII 结构。
- 问题 2:smart pointer 与 resource leak。 比较纹理缓存、独占 socket、父子节点图和只读函数参数应使用什么类型。
- 问题 3:验证构造回滚。 Session 有 socket、buffer、registration 三个 RAII 成员,设计每一步失败测试。