(原文链接: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”。
然而,这段代码太难读了,很难弄清哪个形参对应哪个实参。例如,下面的代码不小心混淆了precision
和thousands_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结构体变成了一个废话更多的复杂的类。
更简单的方法是改用文章开头的指定初始化。
结论
-
如果函数参数多到容易把调用者整懵,或者你想指定参数默认值却不想关心参数顺序,强烈建议考虑使用Option结构体使代码又好用又好看。
-
如果函数参数有Option结构体,用指定初始化可以既减少代码长度又避开临时变量生存期问题。
-
指定初始化既简单又清晰,所以建议让函数接收Option结构体,而不是一长串参数。