Item 17:以独立语句把 newed object 存入 smart pointer
对齐 Effective C++ 第三版 Item 17:解释旧函数实参求值交错导致的 ownership gap,以独立 smart pointer 语句和 make_unique/make_shared 消除泄漏,并说明 C++17 sequencing。
学习目标
- 能复现旧标准中
new、smart pointer construction 与其他 function argument evaluation 交错造成的 resource leak - 能实现 standalone smart-pointer statement、make_unique/make_shared 与 owning factory,明确 borrow 和 ownership transfer
- 能比较 C++17 sequencing、make_shared control block 生命周期和 custom deleter 例外,并设计失败注入测试
Item 17 ownership lab
先预测:priority() 抛异常时,谁负责 Widget?
切换标准规则,再注入或取消异常;观察 evaluation order 如何改变 resource leak 的证据。
owner 状态
尚未构造
resource ledger
会泄漏
审查动作
把 owner 提前建立
从一个看似安全的函数调用开始
原书示例等价于:
processWidget(
std::shared_ptr<Widget>(new Widget),
priority());表面上 raw pointer 立刻写进 shared_ptr constructor,但函数调用包含多个可独立求值步骤。
↡函数调用中为每个实参计算值并执行其子表达式的过程。 ↡new 成功到 smart pointer constructor 完成接管之间,资源暂时没有 owner 的区间。在原书对应的旧规则下,编译器可先执行 new Widget,再执行 priority(),最后构造 shared_ptr。
异常如何落入 gap
若 priority 抛异常,Widget 已分配却没有管理对象:
new Widget succeeds
-> priority() throws
-> stack unwinds
-> shared_ptr was never constructed
-> no destructor owns WidgetRAII 只有 owner 真正存在后才生效。
↡resource allocation 成功后因后续实参抛异常而永远丢失 raw pointer 的泄漏。这不是 shared_ptr destructor 的问题,而是它尚未开始生命周期。
原书修复:独立语句先建立 owner
↡一个完整表达式结束点,所有 value computation 与 side effects 必须完成后才进入下一语句。std::shared_ptr<Widget> widget(new Widget);
processWidget(widget, priority());第一条完整语句结束时 shared_ptr 已构造。priority 再抛异常,widget destructor 在 stack unwinding 中 delete Widget。
↡在单独完整语句中完成 raw resource 到 smart pointer owner 的交接。正是 Item 17 的原始建议:Store newed objects in smart pointers in standalone statements(以独立语句把 newed objects 存入 smart pointers)。
现代首选 make_unique
↡分配构造对象并直接返回 unique_ptr、不给调用方暴露 raw new 的标准 factory。auto widget = std::make_unique<Widget>();
processWidget(*widget, priority()); // borrow
consumeWidget(std::move(widget)); // transfer ownershipowner 在单一 library call 中建立,类型只写一次,异常清理由实现保证。
↡函数签名和调用表达式明确区分临时借用与取得 unique ownership。*widget 是 borrow,std::move(widget) 是 transfer。
make_shared 的安全与分配优势
↡分配构造对象并返回 shared_ptr,通常把对象和 control block 放入一次 allocation。auto widget = std::make_shared<Widget>();
processShared(widget, priority());相比 shared_ptr<Widget>(new Widget) 通常减少一次 allocation,改善局部性,也没有 raw ownership gap。
make_shared 并非所有场景必选,需理解生命周期与 deleter。
weak_ptr 可能延长整块 allocation
make_shared 将对象与 control block 合并。strong count 归零时对象 destructor 执行,但仍有 weak_ptr 时 control block 必须保留,整块 allocation 可能直到 weak count 归零才释放。
↡对象生命周期已结束,但包含 control block 的合并存储因 weak observers 仍存在而延迟归还。大对象且 weak_ptr 长寿命时,分离 shared_ptr(new T) 可能更早释放对象存储,代价是两次分配。
需要测量真实内存压力。
custom deleter 与 make_shared
↡shared/unique owner 析构时用于替代普通 delete 的释放策略对象。make_shared 不接受任意 object deleter;平台资源或特殊 pool 可能需要:
std::shared_ptr<Resource> resource(
acquire_resource(),
[](Resource* value) noexcept { release_resource(value); });仍应让 acquire 结果立即进入这一 constructor,最好由 factory 封装,避免调用点重复 raw pattern。
owning factory 消除调用点 new
↡内部完成资源创建与 owner 绑定,只向调用方返回 smart pointer 的函数。ResourcePtr acquireResource()
{
Resource* raw = platform_acquire();
if (!raw)
throw AcquireError{};
return ResourcePtr{raw, PlatformRelease{}};
}若 deleter 构造可能抛,先设计其 no-throw/value 语义;否则 raw 仍可能泄漏。
C++17 sequencing 改变了什么
↡C++17 起函数各实参求值彼此 indeterminately sequenced,不再交错子表达式,但先后顺序仍未指定。在 C++17 及以后,某一 argument 的完整求值不会与另一个 argument 子求值交错。因此原始 shared_ptr(new Widget), priority() 的特定 leak 场景被语言规则消除。
不过顺序仍未指定:不要让 arguments 依赖彼此 side effects。
f(readState(), mutateState()); // 哪个先仍不应依赖Item 17 的现代价值仍在:避免 raw new、缩短 ownership gap、明确 owner、兼容旧编译模式,并让审查更容易。
历史规则与当前基线都要记录
↡代码必须遵守的最低 C++ 标准版本及其求值顺序保证。维护 C++14 库、嵌入式旧编译器或跨版本 header 时,原风险仍实际存在。文档和 CI 要明确标准模式。
↡同一关键所有权代码在多个标准模式/编译器下构建测试的策略。不能用当前开发机 C++20 的行为替旧目标背书。
多资源函数调用
↡一个函数调用需要多个新资源,每个资源都必须在调用前先进入独立 owner 的场景。auto input = openInput(path);
auto output = openOutput(target);
transform(*input, *output, options());若 openOutput/options 抛,input owner 自动清理。把两个 raw acquire 都塞进 arguments 会让证明复杂。
↡依次建立每个 owner,前一步完成后再进行下一个可能失败操作的资源准备顺序。清楚的多语句代码比压缩成一行更安全可审计。
shared_from_this 与重复 control block
↡同一 raw pointer 被分别交给两个 shared_ptr constructor,形成两个独立 control block 的错误。Widget* raw = new Widget;
std::shared_ptr<Widget> first(raw);
std::shared_ptr<Widget> second(raw); // double deletemake_shared 一开始就建立唯一 control block;需要对象内部获得 shared owner 时使用 enable_shared_from_this,且对象已由 shared_ptr 管理。
constructor 中调用 shared_from_this 也过早,因为 control block 关联可能尚未建立。
owner 建立测试
↡记录 raw acquire、owner construction、其他实参和 release 的顺序与失败点的测试。先预测旧标准三种可能顺序,并指出哪一种泄漏;再比较独立 owner 语句与 make 函数。
- raw acquire 后下一事件是 owner construction,不夹可抛操作。
- priority/options 抛异常时 resource ledger acquire/release 配对。
- multi-resource 第 N 步失败时此前 owner 逆序释放。
- duplicate control block 测试被 factory/API 设计阻止。
- C++14/C++17 构建模式记录求值规则,行为不依赖实参顺序。
- make_shared weak retention 用大对象与 weak lifetime 内存测试量化。
小结
- 原书旧规则下,new、其他 argument 与 smart pointer construction 可交错并形成泄漏
- 独立完整语句先建立 owner,可让后续实参异常由 RAII 清理
- 现代优先 make_unique/make_shared 或 owning factory,避免调用点 raw new
- make_shared 通常单次分配,但 weak_ptr 可延长合并 allocation,custom deleter 也有例外
- C++17 阻止实参子求值交错,但参数先后仍未指定,旧标准目标仍需原规则
- 多资源调用分阶段建立 owner,并用 ownership-handoff trace 验证每个失败点
名词解释
本章出现的专业名词,用大白话再讲一遍。
- function argument evaluation
计算函数每个实参及其子表达式的过程。
- ownership gap
new 成功到 smart pointer 接管之间的无 owner 区间。
- constructed-owner requirement
只有完成构造的 owner 才参与栈展开清理。
- argument-evaluation leak
其他实参异常发生在 owner 建立前造成的泄漏。
- full-expression boundary
一个完整表达式全部求值结束的语句边界。
- standalone smart-pointer statement
独立语句完成 raw 到 smart owner 的交接。
- make_unique
直接创建并返回 unique_ptr 的标准 factory。
- ownership-explicit call
调用表达式明确 borrow 或 transfer ownership。
- control block
保存 shared/weak count 与 deleter 的结构。
- combined-allocation retention
weak observer 使 make_shared 合并存储延迟释放。
- weak-control lifetime
对象销毁后 control state 继续存活的阶段。
- custom deleter
owner 析构时替代 delete 的释放策略。
- deleter-binding construction
raw resource 与 deleter 一次绑定的 owner 构造。
- owning factory
内部完成创建和 owner 绑定并返回 smart pointer 的函数。
- factory ownership handoff
factory 内 acquire 后立即绑定 owner 的规则。
- C++17 argument sequencing
实参彼此不交错但顺序仍未指定的规则。
- indeterminately sequenced
两求值完整先后但顺序不指定的关系。
- language-version baseline
项目要求的最低 C++ 版本及保证。
- cross-standard verification
在多个标准模式验证关键代码的策略。
- multi-resource argument setup
调用前需要建立多个资源 owner 的场景。
- staged ownership establishment
逐个完整建立 owner 再执行下一失败步骤。
- duplicate control block
同一 raw pointer 被多个 shared control block 管理。
- ownership-handoff trace
记录 acquire、owner、实参与 release 顺序的测试。
练习
- 问题 1:复现旧风险。 在 C++14 语义下分析
process(shared_ptr<T>(new T), priority())的可选求值序列。
- 问题 2:选择 make_shared 例外。 大对象有长寿命 weak cache,且另一个资源需 custom deleter,分别设计 owner。
- 问题 3:重构多资源调用。 一个函数参数同时打开输入、输出并计算会抛的 options,设计失败安全顺序。