How absl::StatusOr<T> Handles Move Semantics and Avoids Unnecessary Copies
absl::StatusOr<T> implements move semantics through a carefully designed base class hierarchy that stores data in a union and uses MaybeMoveData() to ensure the wrapped value is moved exactly once, while specialized base classes handle reference types without copying.
The absl::StatusOr<T> class in the Abseil C++ library (abseil/abseil-cpp) provides a discriminated-union-like wrapper that either contains an absl::Status error or a value of type T. Understanding how it handles move semantics is crucial for writing high-performance C++ code that avoids unnecessary copies when propagating return values.
Design Overview: Base Class Architecture
The implementation separates storage concerns from public interface generation through a hierarchy of internal base classes defined in absl/status/internal/statusor_internal.h. This design ensures that moving a StatusOr<T> costs no more than moving the underlying type T itself.
The architecture consists of three key components:
internal_statusor::StatusOrData<T>– Owns the union storage and implements the core move logicinternal_statusor::MoveCtorBase<T>– Provides the public move constructorinternal_statusor::MoveAssignBase<T>– Provides the public move-assignment operator
Union Storage and Active Member Management
StatusOrData<T> stores both the value and status in a union, ensuring that only the active member is constructed or destroyed at any time. This eliminates the possibility of copying inactive data during move operations.
When a StatusOr<T> is moved, the constructor in StatusOrData (lines 78-84 in the internal header) performs the following:
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
}
}
Because the storage is a union, the move operation only touches the active member. If the source contains a value, it moves that value; if it contains an error, it moves the absl::Status instead.
The MaybeMoveData() Optimization
The MaybeMoveData() method centralizes the conditional move logic to ensure the value is moved exactly once. This helper distinguishes between reference and non-reference types:
- For non-reference types: Returns
std::move(data_)to invokeT's move constructor - For reference types: Returns the referenced value unchanged (stored as
Reference<T>), avoiding any copy or move
This specialization ensures that even when T is a reference, the wrapper remains lightweight and copy-free.
Public Interface: Move Constructors and Assignment
The public move operations are deliberately thin, forwarding to the optimized implementations in StatusOrData. According to the source code in absl/status/statusor.h:
MoveCtorBase<T>provides the move constructor that simply forwards toStatusOrData's move constructorMoveAssignBase<T>provides the move-assignment operator that forwards toStatusOrData's move-assignment
Both operations are marked noexcept, allowing the compiler to apply NRVO (Named Return Value Optimization) and other move-friendly optimizations. This design guarantees that code like absl::StatusOr<Heavy> b = std::move(a); performs exactly one move of the underlying Heavy object.
Accessing Values Without Copying
The accessor operators ensure users cannot accidentally trigger copies when retrieving values:
operator*returnsT&(orconst T&) directlyoperator->returnsT*to the stored value
These methods only access the data when ok() returns true, providing zero-overhead value access. Users should always dereference StatusOr using these operators rather than copying the value out.
Practical Example
The following code demonstrates that moving a StatusOr<T> only moves the underlying value once, 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,
"construction failed");
}
int main() {
// Move-construct a StatusOr – only one "move" of Heavy should be printed.
absl::StatusOr<Heavy> a = MakeHeavy(true);
absl::StatusOr<Heavy> b = std::move(a); // moves the stored Heavy
// Move-assign a StatusOr that already holds a value.
absl::StatusOr<Heavy> c = MakeHeavy(true);
absl::StatusOr<Heavy> d = MakeHeavy(true);
d = std::move(c); // moves the stored Heavy
// Access without copying.
if (b.ok()) {
const Heavy& ref = *b; // no copy, just a reference
}
}
Expected output:
move // from MakeHeavy returning a temporary Heavy
move // from std::move(a) into b
move // from std::move(c) into d
The output confirms that only moves occur; the StatusOr wrapper adds no additional copies.
Summary
absl::StatusOr<T>uses union storage ininternal_statusor::StatusOrDatato ensure only active members are movedMaybeMoveData()ensures the value is moved exactly once, with special handling for reference types- Thin public base classes (
MoveCtorBaseandMoveAssignBase) forward to optimized implementations while maintainingnoexceptguarantees - Direct accessors (
operator*andoperator->) prevent accidental copies when retrieving values - All move operations are
noexcept, enabling compiler optimizations like NRVO
Frequently Asked Questions
What happens when I move a StatusOr that contains an error?
When the source StatusOr does not contain a value (!ok()), the move constructor moves the absl::Status object instead of the value. This is implemented in StatusOrData's move constructor by calling MakeStatus(std::move(other.status_)), ensuring the error payload is transferred without copying the status message or payload.
Does StatusOr support move-only types?
Yes, absl::StatusOr<T> fully supports move-only types. Because the implementation uses std::move via MaybeMoveData() and the move constructors are not constrained by copy requirements, you can store std::unique_ptr or other move-only types in a StatusOr. The reference specialization ensures that even reference types don't require copyability.
How does StatusOr avoid copying when T is a reference type?
When T is a reference, absl::StatusOr<T> stores a lightweight Reference<T> wrapper instead of the value directly. The MaybeMoveData() method detects this and returns the reference unchanged rather than attempting to move it, eliminating any copy or move overhead while maintaining reference semantics.
Are StatusOr move operations noexcept?
Yes, the move constructor and move-assignment operator are marked noexcept. This is possible because absl::Status provides noexcept move operations and the value move is invoked via std::move without throwing. The noexcept guarantee allows the compiler to optimize around these operations, particularly when applying Named Return Value Optimization (NRVO) in functions returning StatusOr<T>.
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 →