Last week I had a chat with a few fellow C++ devs, and I was genuinely surprised; some still avoid using std::optional and std::expected. Reason?
This adds extra indirects while access as well as memory allocation.
Basically, there is a belief that they are implemented over pointers. It still pops up more often than you could guess, but it's simply wrong.
Neither std::optional nor std::expected use pointers or heap allocations to store data. Everything lives on the stack - lightweight, fast, and predictable.
Let's finally put that myth to rest.
The std::optional<T> is just a lightweight wrapper around a value of type T, plus a status flag that tells whether it's initialized. No heap or pointer usage at all; conceptually, it looks like:
template <typename T>
struct optional
{
bool is_initialized_;
typename aligned_storage<sizeof(T), alignof(T)>::type storage_;
};
When it is assigned or constructed, the object is placed in place inside storage. If it's empty, the storage just stays uninitialized.
Optional does not allocate memory. So it can do without allocators.
The std::expected<T, E> is very similar in spirit - a lightweight value wrapper, but with additional functionality. Instead of just saying "has value or not", it holds either a value of type T or an error of type E.
Again, no heap or pointer usage - just a union that lives on the stack:
template<class T, class E>
class expected {
bool has_val;
union
{
T val;
E unex;
};
};
When you construct it, either val or unex is placed in place inside the union. Switching between value and error only toggles which part of the union is active.
The std::expected also has another specification for void type; however, this is not relevant in this context and essentially goes pretty close to the std::optional implementation:
template<class T, class E>
class expected {
bool has_val;
union
{
E unex;
};
};
Any object of
expected<T, E>either contains a value of typeTor a value of typeEwithin its own storage. Implementations are not permitted to use additional storage, such as dynamic memory, to allocate the object of typeTor the object of typeE.
Let's summarize - the std::optional and the std::expected are stack-based, safe, and cheap - no hidden allocations.
They behave like any other value wrapper, just adding a status flag (normally 1 byte, rounded by padding).
Use them instead of raw pointers or C-style sentinels.
They're modern C++ tools - fast, expressive, and made for exactly this job.
Do NOT reinvent them with pointers;
Do NOT avoid them thinking they are one;
Do USE them if you need this functionality.
Historical reference, or at least my best guess, what was the origin of this everlasting myth, which finds us even after several new standards after the std::optional introduction.
I was curious what actually creates these misconceptions. Simple answer: we used pointers and NULL as std::nullopt in C code - didn't cut it for me.
During research, I found several articles and comments that summarize to pointers == optional even in the context of C++ and here are a few examples:
What makes an optional an optional?
- It either contains a value or nothing.
- We can query an optional whether it contains a value or not.
- We can retrieve the value if there is any. Turns out: a ordinary pointer just like in C has exactly these properties ... pointers and optionals are quite similar
Also another one:
Rule of thumb (for me) is pointer types are nullable and therefore inherently 'optional'
Quick note regarding these quotes:
I trimmed some context from these quotes but tried not to distort their meaning. My main concern here - comparing pointers and optionals is fundamentally incorrect. The fact that you can use them for similar tasks doesn't make a mallet out of a microscope.
The nullptr is just a convention meaning "points to nothing", but that "nothing" is still something - which is why std::optional is a more precise and safer way to express absence.
Then I stumbled upon this C++ standards paper, and I think I finally saw where all the myths came from - beyond just "similar functionality."
Julius Caesar: "Et tu, Boost?"
Boost: "Not betrayal - legacy."
- 44 BC (~ -6.3 × 10¹⁰ Unix time)
In a comparison between boost::optional and the std::optional proposal, there's a line that says:
"explicit convert to ptr - no [proposal fror std], yes [boost::optional]"
In boost::optional there's a method documented as: "Returns a pointer to the value if this is initialized, otherwise, returns NULL."
This method is (are):
-
get_pointermethod in v1.30 (introduction ofboost::optional); -
get_ptrmethod since v1.31 until nowadays.
Unfortunately, there's no paper or rationale explaining why fetching the value by pointer was considered necessary.
My guess is this ties directly to C-like design habits that still influence modern C++ developers through these myths. Boost's impact on C++ is hard to overstate - and those early interfaces continue to shape misunderstandings about std::optional and std::expected, often leading people to avoid them or, worse, recreate unsafe patterns from the past.
To my knowledge, none of the popular C++ libraries has ever implemented an optional utilizing pointers internally. However, the introduction of this rather confusing get_ptr by boost::optional could mislead some developers. Especially, while at the time of widelly used pointers to provide nulling values in C code-bases.
The naming alone was more than enough to reinforce the illusion of pointer-based implementation and keep this myth alive until today.
P.S. In modern C++, both std::optional and std::expected provide the indirection (operator*) and member access (operator->) operators to access their contained values. These operators express generalized value access rather than raw pointer semantics, making them a clearer and more natural choice than the old boost::optional::get_ptr() method.
Top comments (0)