Google C++每周贴士 #173: 以Option结构体传递参数

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

每周贴士 #173: 以Option结构体传递参数

  • 最初发布于:2019-12-19
  • 作者:John Bandela
  • 更新于:2020-04-06
  • 短链接:abseil.io/tips/173

It came without packages, boxes or bags. And he puzzled and puzzled ‘till his puzzler was sore. – 苏斯博士《格林奇偷走圣诞节》
(译者注:老规矩,起兴部分不翻译,欢迎评论区提供建议)

指定初始化(Designated Initializers)

指定初始化是一个C++20的特性,已经被大部分编译器所支持。指定初始化让Option结构体变得既易用又安全,因为我们可以在调用函数的时候再构造Option对象。这样代码更短,而且能避免很多Option结构体的临时变量生存期问题。

struct PrintDoubleOptions {
  absl::string_view prefix = "";
  int precision = 8;
  char thousands_separator = ',';
  char decimal_separator = '.';
  bool scientific = false;
};

void PrintDouble(double value,
                 const PrintDoubleOptions& options = PrintDoubleOptions{});

std::string name = "my_value";
PrintDouble(5.0, {.prefix = absl::StrCat(name, "="), .scientific = true});

如果想了解为什么Option结构体有用,Option结构体的哪些坑可以用指定初始化来绕开,请接着往下看。

传递多个参数的难点

接收多个参数的函数容易把人搞晕。举个例子,看看下面这个打印浮点数的函数。

void PrintDouble(double value, absl::string_view prefix,  int precision,
                 char thousands_separator, char decimal_separator,
                 bool scientific);

这个函数提供了很大的灵活性,因为它有很多配置项。

PrintDouble(5.0, "my_value=", 2, ',', '.', false);

上面的代码会输出:“my_value=5.00”。

然而,这段代码太难读了,很难弄清哪个形参对应哪个实参。例如,下面的代码不小心混淆了precisionthousands_separator

PrintDouble(5.0, "my_value=", ',', '.', 2, false);

曾经我们在调用端用参数注释来标注参数值的含义,以减少这种混淆。ClangTidy可以根据参数注释来发现上面例子中的错误。

PrintDouble(5.0, "my_value=",
            /*precision=*/2,
            /*thousands_separator=*/',',
            /*decimal_separator=*/'.',
            /*scientific=*/false);

但是,参数注释仍然有一些缺点:

  • 非强制:ClangTidy警报在构建期(buildtime)(译者注:我猜是编译期+链接期,网上没找到准确定义)不起作用。弱智错误(比如没写=)就能悄悄地跳过这个检查,给你一种似有实无的安全感。
  • 可用性:不是所有的项目和平台都支持ClangTidy。

不论你的参数加没加注释,填很多的配置项都会特别冗长。很多时候这些配置项都有合理的默认值。关于这个,我们可以给参数加上默认值。

void PrintDouble(double value, absl::string_view prefix = "", int precision = 8,
                 char thousands_separator = ',', char decimal_separator = '.',
                 bool scientific = false);

现在我们可以在调用PrintDouble的时候少点废话了。

PrintDouble(5.0, "my_value=");

但是,如果想给scientific指定一个非默认值,我们还是得被迫给它前面的参数填值。

PrintDouble(5.0, "my_value=",
            /*precision=*/8,              // unchanged from default
            /*thousands_separator=*/',',  // unchanged from default
            /*decimal_separator=*/'.',    // unchanged from default
            /*scientific=*/true);

如果把所有的配置项都放进一个Option结构体中,那以上所有问题都解决了。

struct PrintDoubleOptions {
  absl::string_view prefix = "";
  int precision = 8;
  char thousands_separator = ',';
  char decimal_separator = '.';
  bool scientific = false;
};

void PrintDouble(double value,
                 const PrintDoubleOptions& options = PrintDoubleOptions{});

现在每个值都有了名字,还可以灵活地运用默认值。

PrintDoubleOptions options;
options.prefix = "my_value=";
PrintDouble(5.0, options);

不过,这个方案也有点毛病。一是在传递Options时引入了新的废话。二是这种风格更容易遇到临时变量生存期问题。

例如,当我们以函数参数传递所有配置项的时候,下面代码是安全的:

std::string name = "my_value";
PrintDouble(5.0, absl::StrCat(name, "="));

在上面的代码中,我们创建了一个临时的string,然后将其绑定到一个string_view。临时变量的生存期是函数调用的整个时间区间,所以我们是安全的。但是以同样的方式改用Options结构体,就会产生一个指飞了的string_view

std::string name = "my_value";
PrintDoubleOptions options;
options.prefix = absl::StrCat(name, "=");
PrintDouble(5.0, options);

两个办法。一种方法是把prefix的类型从string_view改成string。缺点是修改后的Option结构体会比直接传参数效率低。另一种方法是给数据成员添加set方法。

class PrintDoubleOptions {
 public:
  PrintDoubleOptions& set_prefix(absl::string_view prefix) {
    prefix_ = prefix;
    return *this;
  }

  absl::string_view prefix() const { return prefix_; }

  // 其他数据成员的的set和get方法。

 private:
  absl::string_view prefix_ = "";
  int precision_ = 8;
  char thousands_separator_ = ',';
  char decimal_separator_ = '.';
  bool scientific_ = false;
};

这样就可以在调用端设置对应的变量了。

std::string name = "my_value";
PrintDouble(5.0, PrintDoubleOptions{}.set_prefix(absl::StrCat(name, "=")));

译者注:上面例子中临时字符串的生存期会持续到当前语句结束(遇到的第一个分号),也就是函数调用结束,所以是安全的。

你看看,代价是Option结构体变成了一个废话更多的复杂的类。

更简单的方法是改用文章开头的指定初始化。

结论

  1. 如果函数参数多到容易把调用者整懵,或者你想指定参数默认值却不想关心参数顺序,强烈建议考虑使用Option结构体使代码又好用又好看。

  2. 如果函数参数有Option结构体,用指定初始化可以既减少代码长度又避开临时变量生存期问题。

  3. 指定初始化既简单又清晰,所以建议让函数接收Option结构体,而不是一长串参数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值