From 69b25cb42aba171ae357b044f82bcc5b0791ce1c Mon Sep 17 00:00:00 2001
From: Peter Dimov
…
+It makes intuitive sense that variant<X, Y, Z>
can hold only values
+of type X
, type Y
, or type Z
, and nothing else.
If we think of variant
as an extension of union
, since a union
+has a state called "no active member", an argument can be made that a
+variant<X, Y, Z>
should also have such an additional state, holding
+none of X
, Y
, Z
.
This however makes variant
less convenient in practice and less useful
+as a building block. If we really need a variable that only holds X
,
+Y
, or Z
, the additional empty state creates complications that need
+to be worked around. And in the case where we do need this additional
+empty state, we can just use variant<empty, X, Y, Z>
, with a suitable
+struct empty {};
.
From a pure design perspective, the case for no additional empty state is +solid. Implementation considerations, however, argue otherwise.
+When we replace the current value of the variant
(of, say, type X
) with
+another (of type Y
), since the new value needs to occupy the same storage
+as the old one, we need to destroy the old X
first, then construct a new
+Y
in its place. But since this is C++, the construction can fail with an
+exception. At this point the variant
is in the "has no active member"
+state that we’ve agreed it cannot be in.
This is a legitimate problem, and it is this problem that makes having
+an empty/valueless state so appealing. We just leave the variant
empty on
+exception and we’re done.
As explained, though, this is undesirable from a design perspective as it +makes the component less useful and less elegant.
+There are several ways around the issue. The most straightforward one is to
+just disallow types whose construction can throw. Since we can always create
+a temporary value first, then use the move constructor to initialize the one
+in the variant
, it’s enough to require a nonthrowing move constructor,
+rather than all constructors to be nonthrowing.
Unfortunately, under at least one popular standard library implementation,
+node based containers such as std::list
and std::map
have a potentially
+throwing move constructor. Disallowing variant<X, std::map<Y, Z>>
is hardly
+practical, so the exceptional case cannot be avoided.
On exception, we could also construct some other value, leaving the variant
+valid; but in the general case, that construction can also throw. If one of
+the types has a nonthrowing default constructor, we can use it; but if not,
+we can’t.
The approach Boost.Variant takes here is to allocate a temporary copy of
+the value on the heap. On exception, a pointer to that temporary copy can be
+stored into the variant
. Pointer operations don’t throw.
Another option is to use double buffering. If our variant
occupies twice
+the storage, we can construct the new value in the unused half, then, once
+the construction succeeds, destroy the old value in the other half.
When std::variant
was standardized, none of those approaches was deemed
+palatable, as all of them either introduce overhead or are too restrictive
+with respect to the types a variant
can contain. So as a compromise,
+std::variant
took a way that can (noncharitably) be described as "having
+your cake and eating it too."
Since the described exceptional situation is relatively rare, std::variant
+has a special case, called "valueless", into which it goes on exception,
+but the interface acknowledges its existence as little as possible, allowing
+users to pretend that it doesn’t exist.
This is, arguably, not that bad from a practical point of view, but it leaves +many of us wanting. Rare states that "never" occur are undertested and when +that "never" actually happens, it’s usually in the most inconvenient of times.
+This implementation does not follow std::variant
; it statically guarantees
+that variant
is never in a valueless state. The function
+valueless_by_exception
is provided for compatibility, but it always returns
+false
.
Instead, if the contained types are such that it’s not possible to avoid an +exceptional situation when changing the contained value, double storage is +used.
…
+The initial submission only provided the basic exception safety guarantee.
+If an attempt to change the contained value (via assignment or emplace
)
+failed with an exception, and a type with a nonthrowing default constructor
+existed among the alternatives, a value of that type was created into the
+variant
. The upside of this decision was that double storage was needed
+less frequently.
The reviewers were fairly united in hating it. Constructing a random type
+was deemed too unpredictable and not complying with the spirit of the
+basic guarantee. The default constructor of the chosen type, even if
+nonthrowing, may still have undesirable side effects. Or, if not that, a
+value of that type may have special significance for the surrounding code.
+Therefore, some argued, the variant
should either remain with its
+old value, or transition into the new one, without synthesizing other
+states.
At the other side of the spectrum, there were those who considered double +storage unacceptable. But they considered it unacceptable in principle, +regardless of the frequency with which it was used.
+As a result, providing the strong exception safety guarantee on assignment
+and emplace
was declared an acceptance condition.
In retrospect, this was the right decision. The reason the strong guarantee
+is generally not provided is because it doesn’t compose. When X
and Y
+provide the basic guarantee on assignment, so does struct { X x; Y y; };
.
+Similarly, when X
and Y
have nonthrowing assignments, so does the
+struct
. But this doesn’t hold for the strong guarantee.
The usual practice is to provide the basic guarantee on assignment and
+let the user synthesize a "strong" assignment out of either a nonthrowing
+swap
or a nonthrowing move assignment. That is, given x1
and x2
of
+type X
, instead of the "basic" x1 = x2;
, use either X(x2).swap(x1);
+or x1 = X(x2);
.
Nearly all types provide a nonthrowing swap
or a nonthrowing move
+assignment, so this works well. Nearly all, except variant
, which in the
+general case has neither a nonthrowing swap
nor a nonthrowing move
+assignment. If variant
does not provide the strong guarantee itself, it’s
+impossible for the user to synthesize it.
So it should, and so it does.