每周技巧 #176:优先使用返回值,而不是输出参数

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #176: Prefer Return Values to Output Parameters

原文最初作为 TotW #176 发布于 2020 年 3 月 12 日。

作者:Etienne Dechamps

更新于 2020 年 4 月 6 日。

快捷链接:abseil.io/tips/176

问题

考虑下面的函数:

1
2
3
// 从给定 doodad 中提取 foo spec 和 bar spec。
// 如果输入无效,则返回 false。
bool ExtractSpecs(Doodad doodad, FooSpec* foo_spec, BarSpec* bar_spec);

正确使用(或实现)这个函数,要求开发者问自己一堆出乎意料的问题:

  • foo_specbar_spec输出参数,还是输入/输出参数?
  • foo_specbar_spec 中已有的数据会怎样? 是追加?覆盖?让函数 CHECK 失败?让函数返回 false?还是未定义行为?
  • foo_spec 可以为空吗? bar_spec 呢?如果不能,空指针会让函数 CHECK 失败?返回 false?还是未定义行为?
  • foo_specbar_spec 有什么生命周期要求? 换句话说,它们需要比函数调用活得更久吗?
  • 如果返回 false foo_specbar_spec 会怎样?它们保证不变吗?会以某种方式被“重置”吗?还是未指定?

仅凭函数签名无法回答这些问题,也不能依赖 C++ 编译器来强制执行这些契约。函数注释可以帮忙,但往往帮得不够。例如这个函数的文档没有说明大多数问题,而且对 “input” 的含义也有歧义。它只指 doodad,还是也包括其他参数?

此外,这种方式会把样板代码强加给每个调用点:调用者必须提前分配 FooSpecBarSpec 对象,才能调用这个函数。

在这个例子中,有一种简单方式既能消除样板代码,也能用编译器可强制执行的方式编码契约。

解决方案

下面这样可以让所有这些问题都不再成立:

1
2
3
4
5
6
7
struct ExtractSpecsResult {
  FooSpec foo_spec;
  BarSpec bar_spec;
};
// 从给定 doodad 中提取 foo spec 和 bar spec。
// 如果输入无效,则返回 nullopt。
std::optional<ExtractSpecsResult> ExtractSpecs(Doodad doodad);

这个新 API 的语义相同,但现在更难被误用:

  • 输入和输出是什么更清楚。
  • 不存在 foo_specbar_spec 中已有数据如何处理的问题,因为它们由函数从头创建。
  • 不存在空指针问题,因为没有指针。
  • 不存在生命周期问题,因为所有东西都按值传入和返回。
  • 失败时 foo_specbar_spec 会怎样也不成问题,因为如果返回 nullopt,它们甚至无法被访问。

这反过来降低了 bug 概率,也减轻了开发者的认知负担。

还有其他好处。例如,这个函数更容易组合,也就是说,它可以方便地作为更大表达式的一部分使用,例如 SomeFunction(ExtractSpecs(...))

注意事项

  • 这种方式不适用于输入/输出参数。
    • 在某些情况下,可以使用一种变体:按值接收参数、修改它,然后按值返回。这样是否合适,取决于函数如何使用,以及该值是否能高效移动(技巧 #117)。
  • 这种方式不方便调用者自定义返回对象的创建方式。
    • 例如,如果 FooSpecBarSpec 是 protos,输出参数方式允许调用者按需把这些 protos 分配到特定 arena 上。使用返回值方式时,要么把 arena 作为额外参数传入,要么被调用方必须已经知道它。
  • 性能可能因选择的方式和具体场景而不同。
    • 某些情况下,返回值可能较低效,例如在循环中导致重复分配。
    • 其他情况下,得益于 (N)RVO(技巧 #11技巧 #166),返回值可能比你想象的更高效。它甚至可能比输出参数高效,因为优化器不必担心别名问题。
    • 一如既往,避免过早优化。选择最合理的 API,只有在有证据表明性能确实有差异时,才为性能做特殊处理。

建议

  • 只要可行,优先使用返回值,而不是输出参数。这与风格指南一致。
  • 使用 std::optional 这类通用包装器表示缺失的返回值。如果需要更灵活地表示多种备选结果,可以考虑返回 std::variant
  • 使用结构体从函数返回多个值。
    • 如果有意义,可以专门写一个新结构体来表示函数返回值。
    • 抵制为此使用 std::pairstd::tuple 的诱惑。

每周技巧 #175:C++14 和 C++17 中字面常量的变化

上一节

每周技巧 #177:可赋值性与数据成员类型

下一节