Last Update:
Understand internals of std::expected
Table of Contents
In the article about std::expected,
I introduced the type and showed some basic examples, and in this text, you’ll learn how it is implemented.
A simple idea with struct
In short, std::expected
should contain two data members: the actual expected value and the unexpected error object. So, in theory, we could use a simple structure:
template <class _Ty, class _Err>
struct expected {
/*... lots of code ... */
_Ty _Value;
_Err _Unexpected;
};
However, there are better solutions than this. Here are some obvious issues for our “struct” approach.
- The size of the object is the sum of the Value type and the Error type (plus padding if needed).
- Two data members are “active” and initialized, which might not be possible - for example, what if the Value type has no default constructor? The Standard requires that
std::expected" holds either a value of type
Tor an error of type
E` within its storage. - We’d have to guarantee that
_Ty
cannot be a reference type or an array type; it must be aDestructible
Type. - Similarly for the
_Err
type we have to guarantee that it’s alsoDestructible
, and must be a valid template argument forstd::unexpected
(so not an array, non-object type, nor cv-qualified type). - Plus, we’d have to write a lot of code that creates an API for the type
How about std::variant
?
Ok, since we want to have a more compact type, why not use std::variant
?
std::variant
is a tagged union, so it holds only one of the list of types and has an efficient way to switch between them.
We can try with the following code:
template<typename T, typename E>
using expected = std::variant<T, std::unexpected<E>>;
However, std::variant
does not provide some specific behaviors that std::expected
might need, such as conditional explicit constructors and assignment operators based on the contained types’ properties. So, additional work would be required to implement these properly.
Furthermore, std::variant
has to be very generic and offers a way to handle many alternative types in one object, which is “too much” for the expected type, which needs only two alternatives.
Real implementation
Let’s look at some open implementations and see what’s under the hood.
We can go to the Microsoft STL repository:
https://github.com/microsoft/STL/blob/main/stl/inc/expected
The expected
class uses a union to store either a value of type _Ty
or an error of type _Err
:
_EXPORT_STD template <class _Ty, class _Err>
class expected {
/*... lots of code ... */
union {
_Ty _Value;
_Err _Unexpected;
};
bool _Has_value;
};
Here, _Value
and _Unexpected
are two data members that share the same memory location within an instance of expected
. Which member of the union is currently active (i.e., contains valid data) is not tracked by the union itself. Therefore, the expected
class maintains an additional boolean member, _Has_value
, to track whether the union currently holds a _Ty
value (_Has_value
is true) or an _Err
error (_Has_value
is false).
As you can see, this approach is much more advanced than our simple structure and uses some ideas from std::variant
.
Size of Objects:
The size of an expected
object depends on several factors:
- Size of
_Ty
and_Err
: The size of the union will be at least as large as the size of its largest member because the union allocates enough space to hold the largest member. - Alignment: we have to honour
_Ty
’s and_Err
’s alignment requirements. - Boolean
_Has_value
: There’s also a boolean member variable_Has_value
indicating which member of the union is active. This adds to the total size of the class.
So, the total size of an expected<_Ty, _Err>
object will be approximately:
max(sizeof(_Ty), sizeof(_Err)) + sizeof(bool) + possible padding for alignment
Here are some examples of the sizes on GCC x64:
Type: int
Size: 4
Type: std::string
Size: 32
Sizeof std::expected: 40
Type: int
Size: 4
Type: double
Size: 8
Sizeof std::expected: 16
Type: int
Size: 4
Type: std::pair<int, int>
Size: 8
Sizeof std::expected: 12
You can check the code here: @Compiler Explorer.
Going further down
Let’s have a look at some other member functions:
_NODISCARD constexpr const _Ty& value() const& {
if (_Has_value) {
return _Value;
}
_Throw_bad_expected_access_lv();
}
_NODISCARD constexpr _Ty& value() & {
if (_Has_value) {
return _Value;
}
_Throw_bad_expected_access_lv();
}
Those are relatively simple: they check the flag and return the value.
On the other hand, one of the overloads for the assignment operators (see at this line) is more complex:
constexpr expected& operator=(const expected& _Other) noexcept(
is_nothrow_copy_constructible_v<_Ty> && is_nothrow_copy_constructible_v<_Err>
&& is_nothrow_copy_assignable_v<_Ty> && is_nothrow_copy_assignable_v<_Err>) // strengthened
requires _Expected_binary_copy_assignable<_Ty, _Err>
{
if (_Has_value && _Other._Has_value) {
_Value = _Other._Value;
} else if (_Has_value) {
_Reinit_expected(_Unexpected, _Value, _Other._Unexpected);
} else if (_Other._Has_value) {
_Reinit_expected(_Value, _Unexpected, _Other._Value);
} else {
_Unexpected = _Other._Unexpected;
}
_Has_value = _Other._Has_value;
return *this;
}
The _Reinit_expected
template function manages the transition of an std::expected
object from one state to another. Because the code uses union, it has to manage the lifetime of alternatives.
Other implementations
- In GCC libstdc++ - expected @ Github - it also uses union to hold the alternatives
- In LLVM libc++: expected @Github - in many places it uses
no_unique_address
attribute and Empty Base Class Optimization so in theory if might use less space than the regular union approach.
Summary
In this text, I covered a rough idea of how to implement the std::expected
type. As you can see, it’s not just a simple structure of <ValueT, ErrorT>
and contains a lot of tricks to fulfill all of the requirements.
I believe that understanding and appreciating the patterns within the Standard Library can be valuable, even if we don’t need to delve too deeply into its implementation in our everyday C++ tasks.
See the introduction to std::expected
in my previous article.
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: