How is std::optional never "valueless by exception"?
Asked Answered
I

3

14

std::variant can enter a state called "valueless by exception".

As I understand, the common cause of this is if a move assignment throws an exception. The variant's old value isn't guaranteed to be present anymore, and neither is the intended new value.

std::optional, however, doesn't have such a state. cppreference makes the bold claim:

If an exception is thrown, the initialization state of *this ... is unchanged, i.e. if the object contained a value, it still contains a value, and the other way round.

How is std::optional able to avoid becoming "valueless by exception", while std::variant is not?

Impaction answered 28/8, 2019 at 15:55 Comment(0)
P
18

optional<T> has one of two states:

  • a T
  • empty

A variant can only enter the valueless state when transitioning from one state to another if transitioning will throw - because you need to somehow recover the original object and the various strategies for doing so require either extra storage1, heap allocation2, or an empty state3.

But for optional, transitioning from T to empty is just a destruction. So that only throws if T's destructor throws, and really who cares at that point. And transitioning from empty to T is not an issue - if that throws, it's easy to recover the original object: the empty state is empty.

The challenging case is: emplace() when we already had a T. We necessarily need to have destroyed the original object, so what do we do if the emplace construction throws? With optional, we have a known, convenient empty state to fallback to - so the design is just to do that.

variant's problems from not having that easy state to recover to.


1 As boost::variant2 does.
2 As boost::variant does.
3 I'm not sure of a variant implementation that does this, but there was a design suggestion that variant<monostate, A, B> could transition into the monostate state if it held an A and the transition to B threw.

Purify answered 28/8, 2019 at 16:0 Comment(5)
I don't see how this answer addresses the case of an optional<T> going from T to a different T state. Note that emplace and operator= have different behavior here in the case of an exception being thrown in the process!Algebra
@MaxLanghof: If the constructor throws in emplace, then the optional is explicitly stated to be unengaged. If operator= throws during construction, then there's similarly no value. Barry's point remains valid: it works because there is always a legitimate empty state that the optional can go to. variant doesn't have that luxury because variant cannot be empty.Doubledealing
@NicolBolas The difficult case (and the one most similar to the variant problem) is assigning a new value when you have an existing one. And the core of retaining the initialization state is using T::operator= - this specific case involves no empty optional and no destructor at all. Since all the cases covered in this answer regarding std::optional involve either destruction or empty states, I think this important case (covered by the other answers) is missing. Don't get me wrong, this answer covers all the other aspects just fine, but I had to read up on this last case myself...Algebra
@MaxLanghof How's that related to optional? It just does something like **this = *other.Forego
@L.F. That's the important detail - it does not destroy and recreate the contained instance, unlike std::variant (or std::optional::emplace). But I feel this comes down to what parts of the specification one considers obvious and what remains to be explained. The answers here differ in that regard, which should cover the different possible preconceptions of the interface.Algebra
M
8

std::optional has it easy:

  1. It contains a value and a new value is assigned:
    Easy, just delegate to the assignment operator and let it deal with it. Even in the case of an exception, there will still be a value left.

  2. It contains a value and the value is removed:
    Easy, the dtor must not throw. The standard library generally assumes that for user-defined types.

  3. It contains no value and one is assigned:
    Reverting to no value in the face of an exception on constructing is simple enough.

  4. It contains no value and no value is assigned:
    Trivial.

std::variant has the same easy time when the type stored does not change.
Unfortunately, when a different type is assigned it must make place for it by destroying the previous value, and then constructing the new value might throw!

As the previous value is already lost, what can you do?
Mark it as valueless by exception to have a stable, valid though undesirable state, and let the exception propagate.

One could use extra space and time to allocate the values dynamically, save the old value somewhere temporarily, construct the new value before assigning or the like, but all those strategies are costly, and only the first always works.

Melinite answered 28/8, 2019 at 16:5 Comment(0)
E
5

"valueless by exception" refers to a specific scenario where you need to change the type stored in the variant. That necessarily requires 1) destroying the old value and then 2) creating the new one in its place. If 2) fails, you have no way to go back (without undue overhead unacceptable to the committee).

optional doesn't have this problem. If some operation on the object it contains throws an exception, so be it. The object is still there. That doesn't mean that the object's state is still meaningful - it's whatever the throwing operation leaves it in. Hopefully that operation has at least the basic guarantee.

Emie answered 28/8, 2019 at 16:3 Comment(3)
"the initialization state of *this is unchanged" ... am I misunderstanding that statement? I think you're saying that it could change to something not meaningful.Impaction
From optional's perspective, it's still holding an object. Whether that object is in a usable state is not optional's concern.Emie
It is quite an important detail that std::optional::operator= uses T::operator= instead of destroying + constructing the T value. emplace does the latter (and leaves the optional empty if construction of the new value throws).Algebra

© 2022 - 2025 — McMap. All rights reserved.