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 · smart pointers in standalone statements先看 owner 何时出现,再判断 function argument 的 evaluation order旧规则new Widgetraw resourcepriority()another argumentshared_ptr ctorowner arrives lateresource leak安全基线standalone statementmake_unique / make_sharedowner existsfull-expression endsexception-safe callowner releases onceC++17argument A completesno sub-expression interleavingargument B completes先后仍未指定仍要显式 owner不要依赖副作用顺序检查问题异常发生时,资源是否已经落入一个可证明的 owner 生命周期?
关键边界不是“源码写在同一行”,而是 owner 是否在下一次可能失败的 function argument 之前已经完成构造。

Item 17 ownership lab

先预测:priority() 抛异常时,谁负责 Widget?

切换标准规则,再注入或取消异常;观察 evaluation order 如何改变 resource leak 的证据。

new → function argument → owner constructornew Widgetstep 1priority() throwsstep 2stack unwindsstep 3resource leakstep 4观察旧规则允许一个 function argument 的子表达式插入 new 与 shared_ptr constructor 之间。priority() 抛异常时 owner 尚未构造,形成 resource leak。

owner 状态

尚未构造

resource ledger

会泄漏

审查动作

把 owner 提前建立

从一个看似安全的函数调用开始

原书示例等价于:

processWidget(
    std::shared_ptr<Widget>(new Widget),
    priority());

表面上 raw pointer 立刻写进 shared_ptr constructor,但函数调用包含多个可独立求值步骤。

在原书对应的旧规则下,编译器可先执行 new Widget,再执行 priority(),最后构造 shared_ptr。

异常如何落入 gap

若 priority 抛异常,Widget 已分配却没有管理对象:

new Widget succeeds
  -> priority() throws
  -> stack unwinds
  -> shared_ptr was never constructed
  -> no destructor owns Widget

RAII 只有 owner 真正存在后才生效。

这不是 shared_ptr destructor 的问题,而是它尚未开始生命周期。

原书修复:独立语句先建立 owner

std::shared_ptr<Widget> widget(new Widget);
processWidget(widget, priority());

第一条完整语句结束时 shared_ptr 已构造。priority 再抛异常,widget destructor 在 stack unwinding 中 delete Widget。

正是 Item 17 的原始建议:Store newed objects in smart pointers in standalone statements(以独立语句把 newed objects 存入 smart pointers)。

现代首选 make_unique

auto widget = std::make_unique<Widget>();
processWidget(*widget, priority()); // borrow
consumeWidget(std::move(widget));  // transfer ownership

owner 在单一 library call 中建立,类型只写一次,异常清理由实现保证。

*widget 是 borrow,std::move(widget) 是 transfer。

make_shared 的安全与分配优势

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 归零才释放。

大对象且 weak_ptr 长寿命时,分离 shared_ptr(new T) 可能更早释放对象存储,代价是两次分配。

需要测量真实内存压力。

custom deleter 与 make_shared

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

ResourcePtr acquireResource()
{
    Resource* raw = platform_acquire();
    if (!raw)
        throw AcquireError{};
    return ResourcePtr{raw, PlatformRelease{}};
}

若 deleter 构造可能抛,先设计其 no-throw/value 语义;否则 raw 仍可能泄漏。

C++17 sequencing 改变了什么

在 C++17 及以后,某一 argument 的完整求值不会与另一个 argument 子求值交错。因此原始 shared_ptr(new Widget), priority() 的特定 leak 场景被语言规则消除。

不过顺序仍未指定:不要让 arguments 依赖彼此 side effects。

f(readState(), mutateState()); // 哪个先仍不应依赖

Item 17 的现代价值仍在:避免 raw new、缩短 ownership gap、明确 owner、兼容旧编译模式,并让审查更容易。

历史规则与当前基线都要记录

维护 C++14 库、嵌入式旧编译器或跨版本 header 时,原风险仍实际存在。文档和 CI 要明确标准模式。

不能用当前开发机 C++20 的行为替旧目标背书。

多资源函数调用

auto input = openInput(path);
auto output = openOutput(target);
transform(*input, *output, options());

若 openOutput/options 抛,input owner 自动清理。把两个 raw acquire 都塞进 arguments 会让证明复杂。

清楚的多语句代码比压缩成一行更安全可审计。

shared_from_this 与重复 control block

Widget* raw = new Widget;
std::shared_ptr<Widget> first(raw);
std::shared_ptr<Widget> second(raw); // double delete

make_shared 一开始就建立唯一 control block;需要对象内部获得 shared owner 时使用 enable_shared_from_this,且对象已由 shared_ptr 管理。

constructor 中调用 shared_from_this 也过早,因为 control block 关联可能尚未建立。

owner 建立测试

先预测旧标准三种可能顺序,并指出哪一种泄漏;再比较独立 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 验证每个失败点

资料与写作方式声明

本章以Effective C++, Third Edition, Item 17权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

名词解释

本章出现的专业名词,用大白话再讲一遍。

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。

make_shared

通常单次分配对象/control block 并返回 shared_ptr。

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 管理。

shared_from_this ownership

从现有 control block 获取 shared owner 的机制。

ownership-handoff trace

记录 acquire、owner、实参与 release 顺序的测试。

练习

  1. 问题 1:复现旧风险。 在 C++14 语义下分析 process(shared_ptr<T>(new T), priority()) 的可选求值序列。
  1. 问题 2:选择 make_shared 例外。 大对象有长寿命 weak cache,且另一个资源需 custom deleter,分别设计 owner。
  1. 问题 3:重构多资源调用。 一个函数参数同时打开输入、输出并计算会抛的 options,设计失败安全顺序。

讨论

评论区加载中…