每周技巧 #149:对象生命周期与 `= delete`
本节阅读量:本文翻译自 Abseil 官网的 Tip of the Week #149: Object Lifetimes vs. = delete。
原文最初作为 TotW #149 发布于 2018 年 5 月 3 日。
更新于 2020 年 4 月 6 日。
快捷链接:abseil.io/tips/149
钱花光之后再次进入蓝色之中一生一次,地下之水流动 —— David Byrne
生命周期上的 =delete
假设你有一个 API,需要引用某个长生命周期对象,但不取得它的所有权。
|
|
你心想:“嘿,如果有人传了临时对象会怎么样?那会是 bug。不过这是现代 C++,我可以防止它!”于是你拼出一个 API 改动,添加一个删除的重载。
|
|
你对自己的成果很满意,接着想:“嘿,现在 API 已经说明了一切,注释没必要了。”
|
|
这是个好主意吗?为什么?
不要脱离使用场景做设计
照这样展示,你可能会觉得这是个好主意。然而,和许多 API 设计情况一样,盯着 API 定义看很诱人,但看 API 如何使用更有用。所以我们把这个场景重放一遍,把用法考虑进来。
某个用户尝试使用原始的 SetContext(),只想让简单东西先构建起来,又不知道去哪里找正确的 Context 对象,于是直接写出建议调用:
|
|
没有你的 =delete 改动时,这能构建,但会在运行时失败(很可能以神秘方式失败)。查看 SetContext API 时,可以看到生命周期要求已记录,于是代码被修改为符合要求:
|
|
另一方面,用户尝试使用带有你的 =delete 改动、但没有注释的“改进版” SetContext() 时,首先遇到构建失败:
|
|
然后用户会想:“好吧,我不能传临时对象。”但由于没有关于实际要求的信息,最可能的修复是什么?
|
|
现在,问题核心来了:新的自动变量 context 的作用域正好是这个调用需要的生命周期的概率有多大?如果你的答案不是 100%,生命周期要求注释仍然必要。
|
|
以这种方式删除重载集中的一个成员,充其量只是半措施。是的,你避免了一类 bug,但也让 API 复杂化。依赖这种设计一定会带来虚假的安全感:C++ 类型系统根本无法编码参数生命周期要求所需的细节。
既然类型系统实际上无法把这件事做对,我们建议不要用半措施把事情复杂化。保持简单:不要试图依赖这种模式来禁止临时对象,它不够有效,帮不上忙。
用于“优化”的 =delete
把情况反过来:也许你不是想防止临时对象,而是想防止拷贝。
|
|
你的 API 调用方永远不需要保留自己的值,这种可能性有多高?如果不能 100% 确定自己精确知道 API 会如何被使用,这就是惹恼用户的配方。考虑在普通(未删除)设计下拷贝并调用这样的 API:
|
|
既然我们看到第二次扫描是 s 的最后一次使用,就可以直接 std::move 到消耗值的调用中。使用“聪明优化”的版本后,代码看起来更凌乱。
|
|
API 是作为构建块和抽象提供的;API 生态是一个平台,可以用新的、令人惊讶的方式组合在一起,超出任何单个 API 提供者的预测。相信自己确切知道没人应该进行拷贝,与这一点相悖。此外,当移动足够时仍然拷贝造成的低效问题,远比任何单个 API 更广泛,可能更适合用性能分析、培训、代码评审和静态分析的某种组合来解决。
在少数你确实能确定某个 API 必须以特定方式使用的情况下:你很可能应该把它编码进相关类型中。不要用 std::string 作为 DNA 序列的表示来操作;写一个 Dna 类,让它只可移动,并提供一个显式(易于搜索)的方式来执行昂贵拷贝。换句话说:类型的属性应该表达在这些类型中,而不是表达在操作它们的 API 中。
ref 限定
顺带一提:同样的推理也可以应用于破坏性访问器上的 ref 限定符。考虑类似 std::stringbuf 的类;在 C++20 中,它获得了一个消费所含字符串的访问器,并和已有访问器一起呈现为一个重载集:
|
|
(关于引用限定方法和重载集,更多信息见 技巧 #148。)观察 std::stringbuf 的现有用法,几乎每个用法都是一个 stringbuf 用来产生一个字符串。如果忽略旧代码,强制这一点并且只提供“高效”的引用限定破坏性成员,不是最好吗?
当然不是,原因类似前面的 DnaScan 例子:你无法确切知道没人需要它,而且提供 const 重载并不不安全。只有在 ref 限定符作为优化重载集使用,或者它们对于强制语义正确性来说必要时,才使用 ref 限定符。
总结
试图结合右值引用或引用限定符与 =delete,提供更“用户友好”的 API、强制生命周期或防止优化问题,这很诱人。实践中,这些诱惑通常很糟糕。生命周期要求远比 C++ 类型系统能表达的复杂。API 提供者很少能预测其 API 未来所有有效用法。避免这类 =delete 技巧能让事情保持简单。