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.

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...
absl::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();  // 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.

// 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, we can avoid the suffix, and simply name the variable after its underlying value (as we do with pointers).

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

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), should always verify that the code has first verified that the value is present. This validation should be close to where the value is accessed.

  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 . Though exceptions are not enabled by default in google3, StatusOr will soon be made public (where some code will be built with exceptions) and for portable code, the best practices at go/exception-safety also recommend using constructs which behave reasonably whether or not exceptions are enabled. 


Subscribe to the Abseil Blog