每周技巧 #141:小心到 `bool` 的隐式转换

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #141: Beware Implicit Conversions to bool

原文最初作为 TotW #141 发布于 2018 年 1 月 19 日。

作者:Samuel Freilich

更新于 2020 年 4 月 6 日。

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

两种空指针检查

解引用前检查指针是否为空,对于避免崩溃和 bug 很重要。这可以用两种方式完成:

1
2
3
if (foo) {
  DoSomething(*foo);
}
1
2
3
if (foo != nullptr) {
  DoSomething(*foo);
}

foo 是指针时,这两个条件语句语义相同,但后者的类型检查稍微更严格。C++ 中许多类型都可以隐式转换为 bool,当被指向的类型本身也能转换为 bool 时,需要额外小心。

考虑下面这段代码,它可能有两种非常不同的含义:

1
2
3
4
5
6
7
bool* is_migrated = ...;

// 这是在检查 `is_migrated` 非空,还是实际意图是验证
// `*is_migrated` 为 true?
if (is_migrated) {
  ...
}

下面的代码更清楚:

1
2
3
4
// 看起来像对 bool* 的空指针检查
if (is_migrated != nullptr) {
  ...
}

这两种风格在 Google C++ 代码中都可以接受。因此,当底层类型不能隐式转换为 bool 时,跟随周围代码的风格即可。如果相关值是类似 std::unique_ptr 的“智能指针”,语义和权衡也是一样的。

optional 值和带作用域的赋值

optional(例如 std::optional)值又如何?它们值得更仔细考虑。

例如:

1
2
std::optional<bool> b = MaybeBool();
if (b) { ... }  // 当函数返回 std::optional(false) 时会发生什么?

把变量声明放在 if 语句的条件中,可以限制变量作用域,但这个值会被隐式1转换为 bool,因此被测试的是哪个布尔性质可能并不明确。

下面代码的意图更清楚:

1
2
std::optional<bool> b = MaybeBool();
if (b.has_value()) { ... }

注意,事实上上面两个代码片段是等价的:std::optionalbool 的转换只看这个 optional 是否有值,而不是看其中内容。读者可能会觉得 optional(false)true 反直觉,但 optional(false) 有值这一点立刻就很清楚。再次强调,当底层类型可隐式转换为 bool 时,值得额外小心。

optional 返回值的一种模式,是把变量声明放在 if 语句的条件中。这会限制变量作用域,但涉及到 bool 的隐式转换:

1
2
3
if (std::optional<Foo> foo = MaybeFoo()) {
  DoSomething(*foo);
}

注意: 在 C++17 中,if 语句可以包含初始化器,因此可以限制声明作用域,同时避免隐式转换:

1
2
3
if (std::optional<Foo> foo = MaybeFoo(); foo.has_value()) {
  DoSomething(*foo);
}

“类似布尔”的枚举

假设你采纳了 技巧 #94 的建议,决定在函数签名中使用 enum 而不是 bool,以提升调用点可读性。这种重构可能会在函数定义中引入隐式转换:

1
2
3
4
5
6
7
void ParseCommandLineFlags(
    const char* usage, int* argc, char*** argv,
    StripFlagsMode strip_flags_mode) {
  if (strip_flags_mode) {  // 等等,哪个值代表 true 来着?
    ...
  }
}

用显式比较替换隐式转换,可以获得额外清晰性:

1
2
3
4
5
6
7
void ParseCommandLineFlags(
    const char* usage, int* argc, char*** argv,
    StripFlagsMode strip_flags_mode) {
  if (strip_flags_mode == kPreserveFlags) {
    ...
  }
}

总结

总之,要意识到到 bool 的隐式转换可能不清楚,因此可以考虑写得更显式:

  • 把指针类型和 nullptr 比较(尤其是被指向类型可隐式转换为 bool 时)。
  • std::optional<T>::has_value() 这样的具名布尔函数测试 optional 是否有值(尤其是被包含类型可隐式转换为 bool 时)。使用带 optional 初始化器的 if 形式来限制变量作用域(技巧 #165)。不过要记得 call-only 接口,不要取得 value()has_value() 的地址。测试中可以用 testing::Optional matcher 帮忙。
  • 把枚举和具体值比较。

每周技巧 #140:常量:安全惯用法

上一节

每周技巧 #142:多参数构造函数和 `explicit`

下一节

  1. 更准确地说,这是一次上下文转换。 ↩︎