Google C++每周贴士 #176: 返回值优于输出参数

(原文链接:https://abseil.io/tips/176 译者:clangpp@gmail.com)

每周贴士 #176: 返回值优于输出参数

问题

考察如下代码:

// 从给定的`doodad`中提取`foos_spec`和`bar_spec`。
// 当输入非法时返回`false`。
bool ExtractSpecs(Doodad doodad, FooSpec* foo_spec, BarSpec* bar_spec);

正确地使用(或实现)这个函数需要开发者问自己一大坨问题:

  • foo_specbar_spec是输出参数还是输入/输出参数?
  • foo_specbar_spec中已有的数据会发生什么? 是追加在已有数据后面吗?是覆盖掉已有数据吗?它会让函数CHECK挂掉吗(译者注:CHECK是一个宏,接收一个bool值表达式,如果表达式结果是false就让程序崩溃)?会导致函数返回false吗?是未定义行为吗?
  • foo_spec可以是空指针吗? bar_spec呢?如果不可以,空指针会让函数CHECK挂掉吗?空指针会让函数返回false吗?是未定义行为吗?
  • foo_specbar_spec的生存期有要求吗? 换句话说,它们需要比函数调用活得长吗?
  • 如果返回值是falsefoo_specbar_spec会发生什么?它们的值保证不被改变吗?它们会以某种方式被“重置”吗?是未指定行为吗?

你没法仅仅从函数签名得到答案,而且不能依赖C++编译器来保证这些协议。函数注释有点儿用,但是通常没用。比如说,函数的文档,对大部分问题保持缄默,而且在“输入”上有歧义(“输入”仅仅指doodad,还是也包括其他的参数)。

另外,这种方式导致每个调用端都得写一堆废话:为了调用这个函数,调用端不得不预先定义FooSpecBarSpec对象。

这种情况下,有一种简单的方法可以干掉这些废话,并且让编译器保证这些协议。

解决方案

下面是怎样让以上问题都变得毫无意义:

struct ExtractSpecsResult {
  FooSpec foo_spec;
  BarSpec bar_spec;
};
// 从给定的`doodad`中提取`foos_spec`和`bar_spec`。
// 当输入非法时返回`nullopt`。
absl::optional<ExtractSpecsResult> ExtractSpecs(Doodad doodad);

新的API语义不变,但是更难用错:

  • 输入和输出更加清楚。
  • 不再有“foo_specbar_spec中已有数据”的问题,因为它们是在函数中从头开始创建的。
  • 不再有空指针的问题,因为没有指针了。
  • 不再有生存期的问题,因为所有的东西都是以值传递和返回的。
  • 不再有执行失败时foo_specbar_spec会发生什么的问题,因为如果nullopt被返回,它们两个根本就不可能被访问。

因此,这种方式也减少了出bug的可能性,同时减少了开发者的认知负担。

这种方式还有别的好处。例如,函数组合变得更容易;也就是说,它更容易被用在更大的表达式里,比如SomeFunction(ExtractSpecs(...))

  • 这种方式对“输入+输出”参数不起作用。
    • 有时候可以使用这种方式的变体:输入可修改的值类型,以值类型返回。这样好不好取决于函数的调用方式,以及该值类型能否被高效率地移动(贴士#117)。
  • 这种方式不支持在调用端很容易地定制返回对象的创建方式。
    • 举例来说,如果FooSpecBarSpec是proto(译者注:一种数据序列化/反序列化协议,简介博客),“输出参数”方式允许调用者在特定的arena(译者注:proto协议中的概念,预先申请一大块内存以避免堆上小对象的频繁new/delete操作,官方文档)上定义proto。在“以值返回”方式中,这个arena要么作为额外的函数参数,要么函数实现已经知道它了。
  • 不同方式和不同的情况下,效率可能会不同。
    • 有些情况下以值返回效率会比较低,比如(译者注:构造返回值对象时)在循环中多次申请内存。
    • 其他情况下,以值返回可能比你想象得更有效率,多亏了(N)RVO(译者注:(命名)返回值优化)(贴士#11贴士#166)。以值返回可能比输出参数效率更高,因为优化器不用担心别名(aliasing)问题。
    • 老生常谈,避免过早优化。选择最直观的API,直到有证据证明更复杂的API能带来效率优化为止。(译者注:证据通常指性能测试的数据)

建议

  • 只要有可能,返回值优先于输出参数。这和编程风格指南一致。
  • 使用泛型封装器(比如absl::optional)(译者注:也可以用std::optional)表达“返回值缺失”。如果你需要更灵活的选项更多的返回值类型,可以考虑返回absl::variant
  • 使用结构体返回多个值。
    • 如果合理的话,可以定义新的专门的结构体类型来表达某个函数的返回值。
    • 忍住别用std::pairstd::tuple返回多个值。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值