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 appliesstd::moveto the underlying value exactly once whenTis not a reference type.noexceptmove 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 whenStatusOr<T&>is used. - Direct accessors
operator*andoperator->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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →