Option
Option (Rust’s
Option, C++’sstd::optional, Haskell’sMaybe) is a wrapper type that represents either a value or nothing. It’s the simplest sum type, basically.
We are going to construct a struct that can hold a value or nothing and easily switch between the two states. This means that we should decouple the lifetime of the value from the option struct itself.
Note
I hate C++ for its broken naming and love for its flexibility. Anyway, if use a contained type itself, the field most certainly will be constructed and destructed automatically. Do not try to understand first time.
There are three ways to do so:
- Using a pointer to contained type. In this case, the memory will be allocated… somewhere else. So it’s not a good option.
- Using a raw inline storage
alignas(T) char storage[sizeof(T)]. This requires pointer laundering andreinterpret_cast, which is not constexpr safe. - Using a union with a single field of type
T. This is the most straightforward and safe option.
All three require holding a state that indicates whether the option contains a value or nothing. The state is usually a boolean flag or an enum value (in our example). We will discuss the later two options in more detail.
Raw storage
As I said, we need to hold a state that indicates whether the option contains a value or nothing. We will use a enum class, that represents two alternatives: some and none:
enum class state : unsigned char { none = 0, some = 1 };So, the meaningful fields of the option type are:
alignas(T) unsigned char storage[sizeof(T)];
state state = state::none;Note
I’m not going to explain raw storage in detail. This pattern is used in things like stateful allocators, where the stored type might not be known. Anyway, it’s not the topic.
But here are some useful functions and technics you have to use with it:
std::construct_atused to construct the value in-place, basically calls a relevant constructorstd::destroy_atused to destroy the value in-place, basically calls the destructorreinterpret_castused to reinterpret the storage as aT*pointerstd::launderused to “launder” the pointer
So, the three basic operations are:
- Construction
std::construct_at(reinterpret_cast<T*>(storage), );- Destruction
std::destroy_at(reinterpret_cast<T*>(storage));- Access
T value = *std::launder(reinterpret_cast<T*>(storage));
const T const_value = *std::launder(reinterpret_cast<const T*>(storage));Union storage
Union type can hold many alternative types, but only one at a time. And the size of the union is the size of the largest alternative type.
No reinterpret_cast means constexpr, constexpr is good. So the storage field is:
union {
T value;
} storage;Construction and destruction of the value is performed the same way, but the pointer is obtained via &this->storage.value. The value is accessed via this->storage.value. So, the three basic operations are:
- Construction
std::construct_at(&this->storage.value, );- Destruction
std::destroy_at(&this->storage.value);- Access
T value = this->storage.value;This simple change allows us to use the union storage in a constexpr context.
Interface
Knowing basic operations we can implement the interface of option. This is mostly a boilerplate code, as it is composed of the described operations and checks of the current state. More on it in the docs.