Tip of the Week #181: Accessing the value of a StatusOr<T>

Originally posted as TotW #181 on July 9, 2020

By Michael Sheely

Updated 2020-09-02

Quicklink: abseil.io/tips/181

StatusOr<Readability>: you don’t have to choose!

When the time comes to access the value inside an absl::StatusOr<T> object, we should strive to make that access safe, clear, and efficient.

Note: This tip attempts to highlight well lit paths providing guidance for typical use-cases. It is not intended to be exhaustive. If you encounter edge cases, consider the recommendations and reasoning here and exercise judgement.

Recommendation

Accessing the value held by a StatusOr should be performed via operator* or operator->, after a call to ok() has verified that the value is present.

// The same pattern used when handling a unique_ptr...
std::unique_ptr<Foo> foo = TryAllocateFoo();
if (foo != nullptr) {
  foo->DoBar();  // use the value object
}

// ...or an optional value...
std::optional<Foo> foo = MaybeFindFoo();
if (foo.has_value()) {
  foo->DoBar();
}

// ...is also ideal for handling a StatusOr.
absl::StatusOr<Foo> foo = TryCreateFoo();
if (foo.ok()) {
  foo->DoBar();
}

You can limit the scope of a StatusOr by declaring it in the initializer of the if statement and checking ok() in the condition. If using a StatusOr immediately, you generally should limit scope in this way (see Tip #165):

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

Background on StatusOr

The absl::StatusOr<T> class is a tagged union with value semantics indicating exactly one of the following situations:

  • an object of type T is available,
  • an absl::Status error (!ok()) indicating why the value is not present.

You can read more about absl::Status and absl::StatusOr in Tip #76.

Safety, Clarity, and Efficiency

Treating the StatusOr object as one would treat a smart pointer helps code achieve clarity while remaining safe and efficient. Below, we will consider some of the other ways you might have seen a StatusOr accessed, and why we prefer the approach using the indirection operators.

Alternative Value Accessor Safety Issues

What about absl::StatusOr<T>::value()?

absl::StatusOr<Foo> foo = TryCreateFoo();
foo.value().DoBar();  // Behavior depends on the build mode.

Here, the behavior depends on the build mode – in particular, whether the code was compiled with exceptions enabled.1 As such, it is not clear to readers if an error status will terminate the program.

The value() method combines two actions: a test for validity followed by an access of the value. It should therefore be used only if both actions are intended (and even then, think twice and consider that its behavior depends on the build mode). If the status is already known to be OK, then your ideal accessor has semantics of simply accessing the value, which is exactly what operator* and operator-> provide. In addition to making the code more precisely indicate your intent, the access will be at least as efficient as value()’s contract to test for validity and then access the value.

Avoiding Multiple Names for the Same Object

Treating absl::StatusOr objects as we would a smart pointer or optional value also allows us to avoid the conceptually awkward situation of having two variables referring to the same value. It also avoids the naming dilemmas and auto overuse which come with this territory.

// Without digging up the declaration of TryCreateFoo(), a reader will not
// immediately understand the types here (optional? pointer? StatusOr?).
auto maybe_foo = TryCreateFoo();
// ...compounded by the use of implicit bool rather than `.ok()`.
if (!maybe_foo) { /* handle foo not present */ }
// Now two variables (maybe_foo, foo) represent the same value.
Foo& foo = maybe_foo.value();

Avoiding the _or Suffix

Another benefit of using a StatusOr variable’s intrinsic value type after checking for validity (rather than creating multiple variables for the same value) is that we can use the best name for the StatusOr without the need (or temptation!) to add a prefix or suffix.

There is an analogy here to the avoidance of the “_ptr suffix when naming pointer variables.

// The type already describes that this is a unique_ptr; `foo` would be fine.
std::unique_ptr<Foo> foo_ptr;

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

If there is only one variable (the StatusOr, avoiding a second named variable for the unwrapped value), we can drop the suffix, simply naming the variable after its underlying value (just as we do with pointers).

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

Moving from the Value of an absl::StatusOr

We might write code to move from the T of an absl::StatusOr<T>:

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

It’s a little better, though, to move from the StatusOr itself, indicating to readers (both humans and machines) that the entire StatusOr object should not be used after its value has been moved from:

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

Solution

Testing the absl::StatusOr object for validity (as you would a smart pointer or optional) and accessing it using operator* or operator-> is readable, efficient, and safe.

It helps you avoid the potential pitfalls of naming ambiguity mentioned above, and does it all without the use of any macros.

Code that accesses values via operator* or operator-> (whether using a pointer, a StatusOr, an optional, or otherwise) must first verify that the value is present. This validation should be close to where the value is accessed so that readers can easily verify that it is present and correct.

  1. Per the documentation of the value() function, if exceptions are enabled, this will throw absl::BadStatusOrAccess (which may be caught, meaning the program may not terminate). If compiled without exceptions, code will crash with LOG(FATAL)


Subscribe to the Abseil Blog