本周小贴士#181:访问 StatusOr<T> 的值

作为TotW#181最初发表于2020年7月9日

由Michael Sheely创作

当需要访问 absl::StatusOr 对象中的值时,我们应该努力使该访问安全、清晰和高效。

注意:本提示试图强调提供典型用例的指导,但并非详尽无遗。如果遇到边缘情况,请考虑本文中的建议和推理,并行使判断力。

建议

通过 operator* 或 operator-> 访问 StatusOr 持有的值,在调用 ok() 确认值存在后执行。

// 处理 unique_ptr 时使用的相同模式…

std::unique_ptr<Foo> foo = TryAllocateFoo();
if (foo != nullptr) {
  foo->DoBar();  // 使用值对象
}

// ...或可选值...
std::optional<Foo> foo = MaybeFindFoo();
if (foo.has_value()) {
  foo->DoBar();
}

// ...同样适用于处理 StatusOr。
absl::StatusOr<Foo> foo = TryCreateFoo();
if (foo.ok()) {
  foo->DoBar();
}

您可以通过在 if 语句的初始化器中声明并在条件中检查 ok() 限制 StatusOr 的作用域。如果立即使用 StatusOr,则通常应以此方式限制作用域(参见 Tip #165):

if (absl::StatusOr<Foo> foo = TryCreateFoo(); foo.ok()) {
  foo->DoBar();
}

关于 StatusOr 的背景

absl::StatusOr 类是一个带有值语义的标记联合,表示以下情况之一:
类型为 T 的对象可用,
absl::Status 错误(!ok()),表示为什么值不存在。

您可以在 Tip #76 中了解有关 absl::Status 和 absl::StatusOr 的更多信息。

安全、清晰和高效

将 StatusOr 对象视为智能指针一样处理,可以帮助代码实现清晰而安全的效果,同时保持高效。在下面,我们将考虑您可能已经看到的一些其他访问 StatusOr 的方式,以及为什么我们更喜欢使用间接操作符的方法。

替代值访问器的安全问题

那么 absl::StatusOr::value() 怎么办?

absl::StatusOr<Foo> foo = TryCreateFoo();
foo.value(); // 行为取决于构建模式。

在这里,行为取决于构建模式-特别是是否启用异常编译。1 因此,读者不清楚错误状态是否会终止程序。
value() 方法结合了两个操作:有效性测试后跟值的访问。因此,仅在两个操作都打算使用时才应使用它(即使是这样,请再三考虑并考虑到它的行为取决于构建模式)。如果状态已知为 OK,则您的理想访问者具有简单访问值的语义,这正是 operator* 和 operator-> 提供的。除了使代码更准确地指示您的意图外,访问将至少与 value() 的合同一样有效地测试有效性,然后访问值。

避免同一对象的多个名称

将 absl::StatusOr 对象视为智能指针或可选值一样处理,还可以避免概念上尴尬的情况,即有两个变量引用同一个值。它还避免了命名困境和自动使用问题。

//如果不查看TryCreateFoo()的声明,读者将无法立即理解这里的类型(可选?指针?StatusOr?)。
auto maybe_foo = TryCreateFoo();
// ...加上了使用隐式布尔而不是`.ok()`的复杂性。
if (!maybe_foo) { /* 处理foo不存在 */ }
// 现在两个变量(maybe_foo,foo)表示相同的值。
Foo&foo = maybe_foo.value();

避免使用 _or 后缀

使用 StatusOr 变量的内在值类型(而不是为相同值创建多个变量)并在检查有效性后,另一个好处是我们可以使用最佳的 StatusOr 名称,而无需添加前缀或后缀(或者说不会有这种诱惑!)。
这里有一个类比,就像在命名指针变量时避免使用“_ptr”后缀一样。

// 类型已经描述了这是一个 unique_ptr;`foo` 就可以了。
std::unique_ptr<Foo> foo_ptr;

absl::StatusOr<Foo> foo_or = MaybeFoo();
if (foo_or.ok()) {
const Foo& foo = foo_or.value();
foo.DoBar();
}

如果只有一个变量(StatusOr),避免使用第二个命名变量来获取未包装的值,我们可以删除后缀,仅根据其基础值命名变量(就像我们在处理指针时一样)。

const absl::StatusOr<Foo> foo = MaybeFoo();
if (foo.ok()) {
MakeUseOf(*foo);
foo->DoBar();
}

从 absl::StatusOr 的值中移动

我们可能编写代码从 absl::StatusOr 的 T 中移动:

absl::StatusOr<Foo> foo = MaybeFoo();
if (foo.ok()) {
ConsumeFoo(std::move(*foo));
}

不过,更好的方法是从 StatusOr 本身移动,向读者(无论是人还是机器)表明在其值被移动后,整个 StatusOr 对象都不应再被使用:

absl::StatusOr<Foo> foo = MaybeFoo();
if (foo.ok()) {
ConsumeFoo(*std::move(foo));
}

解决方案

测试 absl::StatusOr 对象的有效性(就像智能指针或 optional 一样),并使用 operator* 或 operator-> 访问它是可读性、效率和安全的。
它可以避免上述命名模糊的潜在问题,而且完全不需要使用任何宏。
通过 operator* 或 operator-> 访问值的代码(无论是使用指针、StatusOr、optional 还是其他方式)必须先验证该值是否存在。这个验证应该接近访问该值的位置,以便读者可以轻松地验证它是否存在且正确。
根据 value() 函数的文档,如果启用了异常,则会抛出 absl::BadStatusOrAccess 异常(该异常可以被捕获,这意味着程序可能不会终止)。如果没有启用异常,则代码将崩溃并输出 LOG(FATAL)。 ↩

你可能感兴趣的:(C++,Tips,of,the,Week,c++)