(原文链接:https://abseil.io/tips/176 译者:clangpp@gmail.com)
每周贴士 #176: 返回值优于输出参数
- 最初发布于:2020-03-12
- 作者:Etienne Dechamps
- 更新于:2020-04-06
- 短链接:abseil.io/tips/176
问题
考察如下代码:
// 从给定的`doodad`中提取`foos_spec`和`bar_spec`。
// 当输入非法时返回`false`。
bool ExtractSpecs(Doodad doodad, FooSpec* foo_spec, BarSpec* bar_spec);
正确地使用(或实现)这个函数需要开发者问自己一大坨问题:
foo_spec
和bar_spec
是输出参数还是输入/输出参数?foo_spec
和bar_spec
中已有的数据会发生什么? 是追加在已有数据后面吗?是覆盖掉已有数据吗?它会让函数CHECK挂掉吗(译者注:CHECK是一个宏,接收一个bool值表达式,如果表达式结果是false
就让程序崩溃)?会导致函数返回false
吗?是未定义行为吗?foo_spec
可以是空指针吗?bar_spec
呢?如果不可以,空指针会让函数CHECK挂掉吗?空指针会让函数返回false
吗?是未定义行为吗?- 对
foo_spec
和bar_spec
的生存期有要求吗? 换句话说,它们需要比函数调用活得长吗? - 如果返回值是
false
,foo_spec
和bar_spec
会发生什么?它们的值保证不被改变吗?它们会以某种方式被“重置”吗?是未指定行为吗?
你没法仅仅从函数签名得到答案,而且不能依赖C++编译器来保证这些协议。函数注释有点儿用,但是通常没用。比如说,函数的文档,对大部分问题保持缄默,而且在“输入”上有歧义(“输入”仅仅指doodad
,还是也包括其他的参数)。
另外,这种方式导致每个调用端都得写一堆废话:为了调用这个函数,调用端不得不预先定义FooSpec
和BarSpec
对象。
这种情况下,有一种简单的方法可以干掉这些废话,并且让编译器保证这些协议。
解决方案
下面是怎样让以上问题都变得毫无意义:
struct ExtractSpecsResult {
FooSpec foo_spec;
BarSpec bar_spec;
};
// 从给定的`doodad`中提取`foos_spec`和`bar_spec`。
// 当输入非法时返回`nullopt`。
absl::optional<ExtractSpecsResult> ExtractSpecs(Doodad doodad);
新的API语义不变,但是更难用错:
- 输入和输出更加清楚。
- 不再有“
foo_spec
和bar_spec
中已有数据”的问题,因为它们是在函数中从头开始创建的。 - 不再有空指针的问题,因为没有指针了。
- 不再有生存期的问题,因为所有的东西都是以值传递和返回的。
- 不再有执行失败时
foo_spec
和bar_spec
会发生什么的问题,因为如果nullopt
被返回,它们两个根本就不可能被访问。
因此,这种方式也减少了出bug的可能性,同时减少了开发者的认知负担。
这种方式还有别的好处。例如,函数组合变得更容易;也就是说,它更容易被用在更大的表达式里,比如SomeFunction(ExtractSpecs(...))
。
坑
- 这种方式对“输入+输出”参数不起作用。
- 有时候可以使用这种方式的变体:输入可修改的值类型,以值类型返回。这样好不好取决于函数的调用方式,以及该值类型能否被高效率地移动(贴士#117)。
- 这种方式不支持在调用端很容易地定制返回对象的创建方式。
- 不同方式和不同的情况下,效率可能会不同。