bugprone-unchecked-optional-access

注意:此检查使用流敏感静态分析生成结果。 因此,它可能比一般的 clang-tidy 检查更占用资源(RAM、CPU)。

此检查识别对 std::optional<T>absl::optional<T>base::Optional<T>folly::Optional<T>bsl::optionalBloombergLP::bdlb::NullableValue 对象中包含的值的不安全访问。 下面我们将统称为 optional<T>

当调用其 valueoperator*operator-> 成员函数时,会发生对 optional<T> 值的访问。 为符合常见的误解,该检查认为这些成员函数是等效的,即使在异常与未定义行为方面存在细微差别。 有关此主题的更多信息,请参见下面的其他说明

当且仅当本地范围(例如,函数体)中的代码确保 optional<T> 在所有可能到达访问的执行路径中都具有值时,对 optional<T> 值的访问被认为是安全的。 这应该通过显式检查(使用 optional<T>::has_value 成员函数)或以显示它明确包含一个值的方式构造 optional<T> 来完成(例如,使用 std::make_optional,它总是返回一个填充的 std::optional<T>)。

下面我们列出了一些示例,从不安全的可选访问模式开始,然后是安全的访问模式。

不安全的访问模式

在不检查是否存在的情况下访问值

该检查会标记没有被存在检查本地保护的值访问

void f(std::optional<int> opt) {
  use(*opt); // unsafe: it is unclear whether `opt` has a value.
}

在错误的分支中访问值

该检查了解可选对象在代码不同分支中的状态。 例如

void f(std::optional<int> opt) {
  if (opt.has_value()) {
  } else {
    use(opt.value()); // unsafe: it is clear that `opt` does *not* have a value.
  }
}

假设函数结果是稳定的

该检查知道函数结果可能不稳定。 也就是说,对同一函数的连续调用可能会返回不同的值。 例如

void f(Foo foo) {
  if (foo.opt().has_value()) {
    use(*foo.opt()); // unsafe: it is unclear whether `foo.opt()` has a value.
  }
}

异常:访问器方法

该检查假设类的访问器方法是稳定的,并使用启发式方法来确定哪些方法是访问器。 具体来说,无参数的 const 方法被视为访问器。 请注意,这不能保证是安全的 - 但在实践中被广泛使用(安全),因此我们选择将其视为一般安全。 对非 const 方法的调用被假定为修改对象的状态并影响早期访问器调用的稳定性。

依赖于不常见 API 的不变式

该检查不知道不常见 API 的不变式。 例如

void f(Foo foo) {
  if (foo.HasProperty("bar")) {
    use(*foo.GetProperty("bar")); // unsafe: it is unclear whether `foo.GetProperty("bar")` has a value.
  }
}

检查值是否存在,然后将可选对象传递给另一个函数

该检查依赖于本地推理。 检查和值访问必须都在同一个函数中发生。 即使执行访问的函数的调用者确保可选对象具有值,访问也被认为是不安全的。 例如

void g(std::optional<int> opt) {
  use(*opt); // unsafe: it is unclear whether `opt` has a value.
}

void f(std::optional<int> opt) {
  if (opt.has_value()) {
    g(opt);
  }
}

安全的访问模式

检查值是否存在,然后访问值

该检查识别了所有检查值是否存在和访问可选对象中包含的值的直接方法。 例如

void f(std::optional<int> opt) {
  if (opt.has_value()) {
    use(*opt);
  }
}

检查值是否存在,然后从副本中访问值

该检查使用的标准是语义上的,而不是语法上的。 它会识别何时被访问的可选对象的副本已知具有值。 例如

void f(std::optional<int> opt1) {
  if (opt1.has_value()) {
    std::optional<int> opt2 = opt1;
    use(*opt2);
  }
}

使用常见宏来确保值存在

该检查了解常见的宏,如 CHECKDCHECK。 这些宏可用于确保可选对象具有值。 例如

void f(std::optional<int> opt) {
  DCHECK(opt.has_value());
  use(*opt);
}

确保值存在,然后在相关分支中访问值

该检查了解代码中的相关分支,并且可以确定何时在导致访问的所有执行路径上确保可选对象具有值。 例如

void f(std::optional<int> opt) {
  bool safe = false;
  if (opt.has_value() && SomeOtherCondition()) {
    safe = true;
  }
  // ... more code...
  if (safe) {
    use(*opt);
  }
}

稳定函数结果

由于函数结果不假定在调用之间保持稳定,因此最好将函数调用的结果存储在局部变量中,并使用该变量访问值。 例如

void f(Foo foo) {
  if (const auto& foo_opt = foo.opt(); foo_opt.has_value()) {
    use(*foo_opt);
  }
}

不要依赖于不常见 API 的不变式

当不常见 API 保证可选对象具有内容时,不要依赖它 - 相反,请显式检查可选对象是否具有值。 例如

void f(Foo foo) {
  if (const auto& property = foo.GetProperty("bar")) {
    use(*property);
  }
}

而不是上面看到的 HasPropertyGetProperty 对。

不要依赖于调用者执行的检查

如果您知道函数的所有调用者都已检查可选参数是否具有值,则要么将函数更改为直接获取值,要么在被调用者的本地范围内再次检查可选对象。 例如

void g(int val) {
  use(val);
}

void f(std::optional<int> opt) {
  if (opt.has_value()) {
    g(*opt);
  }
}

struct S {
  std::optional<int> opt;
  int x;
};

void g(const S &s) {
  if (s.opt.has_value() && s.x > 10) {
    use(*s.opt);
}

void f(S s) {
  if (s.opt.has_value()) {
    g(s);
  }
}

其他说明

通过 using 声明创建的别名

该检查了解通过 using 声明创建的可选类型的别名。 例如

using OptionalInt = std::optional<int>;

void f(OptionalInt opt) {
  use(opt.value()); // unsafe: it is unclear whether `opt` has a value.
}

Lambda 表达式

该检查目前不会报告 Lambda 表达式中不安全的可选访问。 将来的版本将扩展到 Lambda 表达式,遵循上述规则。 在 Lambda 表达式中使用可选对象时,最好遵循相同的原则。

使用 operator*()value() 访问

鉴于 value() 具有明确定义的行为(要么抛出异常,要么终止程序),为什么要将其与 operator*() 相同对待,后者会导致未定义行为(UB)? 也就是说,为什么要将使用 value() 访问可选对象视为不安全,如果它没有被证明已填充值? 至于这一点,为什么 CHECK() 后跟 operator*()value() 更好,因为它们在语义上是等效的(在禁用异常的配置上)?

答案是我们假设大多数用户没有意识到 value()operator*() 之间的区别。 转移到 operator*() 以及某种形式的显式值存在检查或显式程序终止具有两个优点

  • 可读性。 检查和任何潜在的副作用(如程序关闭)在代码中非常清晰。 将访问与检查分离实际上可以使检查更加明显。

  • 性能。 一次检查可以涵盖范围内的许多甚至所有访问。 这让用户获得两全其美的体验 - 动态检查的安全性,但不会产生冗余成本。