How absl::StatusOr<T> Handles Move Semantics and Object Invalidation

absl::StatusOr implements move semantics through a discriminated-union architecture centered on internal_statusor::StatusOrData<T>, using MaybeMoveData() to move the underlying value exactly once while maintaining noexcept guarantees to prevent object invalidation.

The absl::StatusOr<T> class in the abseil/abseil-cpp repository is a discriminated-union wrapper that stores either a value of type T or an absl::Status error. Understanding how absl::StatusOr<T> handles move semantics is critical for writing high-performance C++ code that avoids unnecessary copies and prevents object invalidation during error propagation.

Move Semantics Architecture in absl::StatusOr

The implementation separates value handling from status handling through carefully-designed base classes in absl/status/internal/statusor_internal.h. This separation ensures that moving a StatusOr<T> costs no more than moving a plain T.

StatusOrData and Union Storage

The core logic resides in internal_statusor::StatusOrData<T> (see lines 78-84 of absl/status/internal/statusor_internal.h). This class owns the storage for both the value (data_) and the status (status_) in a union, ensuring only the active member is constructed at any time. The move constructor implements the branching logic that determines whether to move the value or the status:

StatusOrData(StatusOrData&& other) noexcept {
  if (other.ok()) {
    MakeValue(other.MaybeMoveData());    // moves the stored T
    MakeStatus();                        // re-creates an OK status
  } else {
    MakeStatus(std::move(other.status_)); // moves the error status
  }
}

When the source contains a value (other.ok() is true), the constructor moves the stored data via MaybeMoveData() and constructs a new OK status. When the source contains an error, it moves the status object instead. This union-based approach eliminates any temporary objects or copying of inactive members.

MaybeMoveData() and Conditional Moves

The MaybeMoveData() method centralizes the logic for conditionally moving the stored value. For non-reference types T, it returns std::move(data_), ensuring the underlying object is moved exactly once. For reference types, it returns the referenced value unchanged, avoiding any copy or move operations on the referenced object itself.

This mechanism prevents unnecessary copies when T is expensive to copy but cheap to move. The public interface classes MoveCtorBase<T> and MoveAssignBase<T> in absl/status/statusor.h provide thin wrappers that forward to these internal implementations, keeping the public API clean while preserving the optimization.

Preventing Object Invalidation

Object invalidation during move operations is prevented through strict noexcept guarantees and careful handling of the moved-from state.

noexcept Guarantees and NRVO

All move constructors and move assignment operators in absl::StatusOr<T> are marked noexcept. This allows the compiler to apply Named Return Value Optimization (NRVO) and other move-elision optimizations safely, ensuring that the underlying object T remains valid after being moved from a StatusOr<T> container. The noexcept specification guarantees that if a move operation begins, it will complete without throwing exceptions, preventing the object from being left in an indeterminate state.

Reference Specialization

When T is a reference type, absl::StatusOr<T> stores a lightweight Reference<T> object instead of the value directly. This specialization forwards the reference without any copy or move of the underlying object, preventing invalidation of the referenced object entirely. This design is particularly important when StatusOr wraps large objects or resources that must remain valid after the StatusOr container is moved.

Practical Code Examples

The following example demonstrates that absl::StatusOr<T> performs exactly one move of the underlying type, with no extra copies introduced by the wrapper:

#include "absl/status/statusor.h"
#include "absl/status/status.h"
#include <iostream>

struct Heavy {
  Heavy() = default;
  Heavy(const Heavy&) { std::cout << "copy\n"; }
  Heavy(Heavy&&) noexcept { std::cout << "move\n"; }
};

absl::StatusOr<Heavy> MakeHeavy(bool succeed) {
  if (succeed) return Heavy();
  return absl::Status(absl::StatusCode::kInvalidArgument, "failed");
}

int main() {
  // Move-construct: only one move of Heavy
  absl::StatusOr<Heavy> a = MakeHeavy(true);
  absl::StatusOr<Heavy> b = std::move(a);

  // Move-assign between two StatusOr objects holding values
  absl::StatusOr<Heavy> c = MakeHeavy(true);
  absl::StatusOr<Heavy> d = MakeHeavy(true);
  d = std::move(c);

  // Access without copying
  if (b.ok()) {
    const Heavy& ref = *b;  // No copy, direct reference to stored value
  }
}

Output:


move
move
move

The output confirms that absl::StatusOr<T> moves the Heavy object exactly once per transfer, with no copies generated by the wrapper itself. Direct access via operator* and operator-> returns references (T& or T*) to the stored value without any copy or move operations.

Summary

  • Union storage in StatusOrData<T> ensures only the active member (value or status) is moved, eliminating copy overhead from inactive members.
  • MaybeMoveData() conditionally applies std::move to the underlying value exactly once when T is not a reference type.
  • noexcept move operations guarantee that objects are not left in invalid states during moves, enabling compiler optimizations like NRVO.
  • Reference specialization stores references via Reference<T>, avoiding any copy or move of the referenced object when StatusOr<T&> is used.
  • Direct accessors operator* and operator-> provide reference access to the stored value without intermediate copies.

Frequently Asked Questions

Is absl::StatusOr move-only?

No, absl::StatusOr<T> is both moveable and copyable, provided that T supports the respective operations. However, the implementation is optimized to prefer moves over copies. If T is move-only (e.g., std::unique_ptr), then StatusOr<T> becomes move-only as well, since the copy operations are deleted when T is not copy-constructible or copy-assignable.

What happens to the source StatusOr after a move?

After a StatusOr<T> is moved from, it remains in a valid but unspecified state. Typically, if the source contained a value, it will contain a moved-from instance of T (valid but unspecified per C++ standard), and the status will be reset to an OK state. If the source contained an error, the status is moved-from but remains valid. You should not access the value of a moved-from StatusOr without checking ok() first.

Does StatusOr support copy semantics?

Yes, absl::StatusOr<T> supports copy construction and copy assignment when T is copyable. The copy operations are implemented in StatusOrData<T> and perform a copy of the active member (either the value via copy constructor or the status). However, for performance-critical code, moves should be preferred to avoid the cost of copying large objects or deeply nested status messages.

How does StatusOr handle moves for reference types?

When T is a reference type (e.g., StatusOr<int&>), the internal storage uses Reference<T> which simply holds a pointer to the referenced object. Moving a StatusOr<T&> copies this pointer rather than the referenced object, ensuring the object referred to remains valid and unmoved. This specialization prevents accidental invalidation of objects accessed through StatusOr references.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →