From 69b25cb42aba171ae357b044f82bcc5b0791ce1c Mon Sep 17 00:00:00 2001 From: Peter Dimov Date: Mon, 13 May 2019 19:15:53 +0300 Subject: [PATCH] Add design rationale --- doc/html/variant2.html | 148 ++++++++++++++++++++++++++++++++++++++- doc/variant2/design.adoc | 123 +++++++++++++++++++++++++++++++- 2 files changed, 266 insertions(+), 5 deletions(-) diff --git a/doc/html/variant2.html b/doc/html/variant2.html index ab14090..2d54afd 100644 --- a/doc/html/variant2.html +++ b/doc/html/variant2.html @@ -929,13 +929,155 @@ contained types have a non-throwing move constructor.

Never Valueless

-

…​

+

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.

Strong Exception Safety

-

…​

+

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.

@@ -2643,7 +2785,7 @@ the Boost Software License, Versi