Motivational example
When I write:
std::optional<X*> opt{};
(*opt)->f();//expects error here, not UB or heap corruption
I would expect the optional is initialized and doesn't contain uninitialized memory. Also I wouldn't expect a heap corruption to be a consequence since Im expecting everything is initialized fine. This compares up with the pointer semantic of std::optional
:
X* ptr{};//ptr is now zero
ptr->f();//deterministic error here, not UB or heap corruption
If I write std::optional<X*>(std::nullopt)
I would have hoped the same but at least here it looks more of an ambiguous situation.
The reason is Uninitialized Memory
It is very likely that this behavior is intentional.
(Im not part of any comittee so in the end I cannot say sure)
This is the primary reason: an empty brace init (zero-init) shouldn't lead to uninitialized memory (although the language doesn't enforce this as a rule) - how else will you guarentee there's no un-initialized memory in your program ?
For this task we often turn to use static analysis tools: prominently cpp core check that is based on enforcing the cpp core guidelines; in particular there's a few guidelines concerning exactly this issue. Had this not been possible our static analysis would fail for this otherwise seemingly simple case; or worse be misleading. In contrast, heap based containers do not have the same issue naturally.
Unchecked access
Remember that accessing std::optional
is unchecked - this leads to the case where you could by mistake access that unitialized memory.
Just to showcase this, if that weren't the case then this could be heap corruption:
std::optional<X*> opt{};//lets assume brace-init doesn't zero-initialize the underlying object for a moment (in practice it does)
(*opt)->f();//<- possible heap corruption
With current implementation however, this becomes deterministic (seg fault/access violation on main platforms).
Then you might ask, why doesn't the std::nullopt
'specialized' constructor not initialize the memory ?
Im not really sure why it doesn't. While I guess it wouldn't be an issue if it did. In this case, as opposed to the brace-init one, it doesn't come with the same kind of expectations. Subtly, you now have a choice.
For those interested MSVC does the same.
std::nullopt
most of the time because it's more explicit and reduces confusion and as you shown, sometimes{}
acts as default init instead of what you think it would. – Vegetable{}
or()
; regardless of whetherData
is Plain Old Data or has a defined default constructor. – Hodeidanullopt1
andnullopt2
act differently, but that's probably not in the scope of this question. – Aimo