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?

Community
  • 1
  • 1
Drew Dormann
  • 59,987
  • 13
  • 123
  • 180

3 Answers3

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.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • I don't see how this answer addresses the case of an `optional` 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! – Max Langhof Aug 28 '19 at 16:45
  • @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. – Nicol Bolas Aug 28 '19 at 16:50
  • @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... – Max Langhof Aug 28 '19 at 16:56
  • @MaxLanghof How's that related to `optional`? It just does something like `**this = *other`. – L. F. Aug 29 '19 at 03:35
  • @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. – Max Langhof Aug 29 '19 at 07:40
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.

Deduplicator
  • 44,692
  • 7
  • 66
  • 118
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.

T.C.
  • 133,968
  • 17
  • 288
  • 421
  • "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. – Drew Dormann Aug 28 '19 at 16:05
  • 2
    From `optional`'s perspective, it's still holding an object. Whether that object is in a usable state is not `optional`'s concern. – T.C. Aug 28 '19 at 16:07
  • 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). – Max Langhof Aug 28 '19 at 16:43