每周技巧 #176:优先使用返回值,而不是输出参数
本节阅读量:本文翻译自 Abseil 官网的 Tip of the Week #176: Prefer Return Values to Output Parameters。
原文最初作为 TotW #176 发布于 2020 年 3 月 12 日。
更新于 2020 年 4 月 6 日。
快捷链接:abseil.io/tips/176
问题
考虑下面的函数:
|
|
正确使用(或实现)这个函数,要求开发者问自己一堆出乎意料的问题:
foo_spec和bar_spec是输出参数,还是输入/输出参数?foo_spec或bar_spec中已有的数据会怎样? 是追加?覆盖?让函数 CHECK 失败?让函数返回false?还是未定义行为?foo_spec可以为空吗?bar_spec呢?如果不能,空指针会让函数 CHECK 失败?返回false?还是未定义行为?foo_spec和bar_spec有什么生命周期要求? 换句话说,它们需要比函数调用活得更久吗?- 如果返回
false,foo_spec和bar_spec会怎样?它们保证不变吗?会以某种方式被“重置”吗?还是未指定?
仅凭函数签名无法回答这些问题,也不能依赖 C++ 编译器来强制执行这些契约。函数注释可以帮忙,但往往帮得不够。例如这个函数的文档没有说明大多数问题,而且对 “input” 的含义也有歧义。它只指 doodad,还是也包括其他参数?
此外,这种方式会把样板代码强加给每个调用点:调用者必须提前分配 FooSpec 和 BarSpec 对象,才能调用这个函数。
在这个例子中,有一种简单方式既能消除样板代码,也能用编译器可强制执行的方式编码契约。
解决方案
下面这样可以让所有这些问题都不再成立:
|
|
这个新 API 的语义相同,但现在更难被误用:
- 输入和输出是什么更清楚。
- 不存在
foo_spec和bar_spec中已有数据如何处理的问题,因为它们由函数从头创建。 - 不存在空指针问题,因为没有指针。
- 不存在生命周期问题,因为所有东西都按值传入和返回。
- 失败时
foo_spec和bar_spec会怎样也不成问题,因为如果返回nullopt,它们甚至无法被访问。
这反过来降低了 bug 概率,也减轻了开发者的认知负担。
还有其他好处。例如,这个函数更容易组合,也就是说,它可以方便地作为更大表达式的一部分使用,例如 SomeFunction(ExtractSpecs(...))。
注意事项
- 这种方式不适用于输入/输出参数。
- 在某些情况下,可以使用一种变体:按值接收参数、修改它,然后按值返回。这样是否合适,取决于函数如何使用,以及该值是否能高效移动(技巧 #117)。
- 这种方式不方便调用者自定义返回对象的创建方式。
- 例如,如果
FooSpec和BarSpec是 protos,输出参数方式允许调用者按需把这些 protos 分配到特定 arena 上。使用返回值方式时,要么把 arena 作为额外参数传入,要么被调用方必须已经知道它。
- 例如,如果
- 性能可能因选择的方式和具体场景而不同。
建议
- 只要可行,优先使用返回值,而不是输出参数。这与风格指南一致。
- 使用
std::optional这类通用包装器表示缺失的返回值。如果需要更灵活地表示多种备选结果,可以考虑返回std::variant。 - 使用结构体从函数返回多个值。
- 如果有意义,可以专门写一个新结构体来表示函数返回值。
- 请抵制为此使用
std::pair或std::tuple的诱惑。
本节目录