I have seen the following notes on cppreference regarding to the valueless_by_exception
method:
A variant may become valueless in the following situations:
- (guaranteed) an exception is thrown during the move initialization of the contained value during move assignment
- (optionally) an exception is thrown during the copy initialization of the contained value during copy assignment
So, the code like this
std::variant<MyClass> var = {...};
var = myClassObj;
is not required to make var.valueless_by_exception()
equal to true (and, presumably, would leave var
in its previous state), but this code
std::variant<MyClass> var = {...};
var = std::move(myClassObj);
is guaranteed to make var.valueless_by_exception()
equal to true if an exception happens.
What is the actual reason for such a difference in specifications between copy and move assignment?
CodePudding user response:
Note that Cppreference is talking about a different situation. When it says copy/move assignment, it's talking about copy/move assignment from a variant, not from a T
. Assignment from T
is dealt with in the next statement:
(optionally) an exception is thrown when initializing the contained value during a type-changing assignment
The reason failed move assignment from a variant will always leave the target variant valueless is that there's no other option.
The issue at hand here is something specific to assignment. When you are assigning one variant
to the other, there are two possibilities. Either the two variants hold the same Ti
type, or they don't.
If they share the same Ti
, then a copy/move assignment between their stored types can happen directly. When that happens, variant provides the exception guarantee of the assignment operation for the Ti
being used. At no point does the variant itself become valueless; it always holds a live Ti
which is in whatever state the failed copy/move assignment operator left it in.
However, if the two variants differ on Ti
, then the destination variant must destroy its current object and create a new object of the source type Ti
via copy/move construction (not assignment).
If variant were to provide a strong exception guarantee, that would mean that if this copy/move construction fails, then the variant should still hold the object type and value it had before the assignment operator. But you'll notice that this object was destroyed before the creation of the new object. This is because variant
stores a union of all of its Ts
, and thus they all share memory. You can't attempt to create a Ti
in the variant without first destroying whatever was there.
And once its destroyed, it's gone and cannot be recovered.
So now we have a variant that doesn't hold the original type, and we failed to create a Ti
within it. So... what does it hold?
Nothing.
The reason why going valueless from a copy is optional is that, if a type throws on copy construction but not on move construction (as many throwing-copy types provide), it is possible to implement the copy operation with a two object solution. You do the copy-initialization to a stack temporary before destroying the internal variant object. If that succeeds, you can destroy the internal object and move-construct from your stack object.
The committee didn't want to require this implementation, but it didn't want to forbid it either.