forked from boostorg/variant2
190 lines
8.5 KiB
Plaintext
190 lines
8.5 KiB
Plaintext
////
|
|
Copyright 2018, 2019 Peter Dimov
|
|
|
|
Distributed under the Boost Software License, Version 1.0.
|
|
|
|
See accompanying file LICENSE_1_0.txt or copy at
|
|
http://www.boost.org/LICENSE_1_0.txt
|
|
////
|
|
|
|
[#design]
|
|
# Design
|
|
:idprefix: design_
|
|
|
|
## Features
|
|
|
|
This `variant` implementation has two distinguishing features:
|
|
|
|
* It's never "valueless", that is, `variant<T1, T2, ..., Tn>` has an
|
|
invariant that it always contains a valid value of one of the types
|
|
`T1`, `T2`, ..., `Tn`.
|
|
* It provides the strong exception safety guarantee on assignment and
|
|
`emplace`.
|
|
|
|
This is achieved with the use of double storage, unless all of the
|
|
contained types have a non-throwing move constructor.
|
|
|
|
## Rationale
|
|
|
|
### 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 {cpp}, 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.
|
|
|
|
## Differences with std::variant
|
|
|
|
The main differences between this implementation and `std::variant` are:
|
|
|
|
* No valueless-by-exception state: `valueless_by_exception()` always
|
|
returns `false`.
|
|
* Strong exception safety guarantee on assignment and `emplace`.
|
|
* `emplace` first constructs the new value and then destroys the old one;
|
|
in the single storage case, this translates to constructing a temporary
|
|
and then moving it into place.
|
|
* A converting constructor from, e.g. `variant<int, float>` to
|
|
`variant<float, double, int>` is provided as an extension.
|
|
* The reverse operation, going from `variant<float, double, int>` to
|
|
`variant<int, float>` is provided as the member function `subset<U...>`.
|
|
(This operation can throw if the current state of the variant cannot be
|
|
represented.)
|
|
* `unsafe_get`, an unchecked alternative to `get` and `get_if`, is provided
|
|
as an extension.
|
|
* `visit_by_index`, a visitation function that takes a single variant and a
|
|
number of function objects, one per alternative, is provided as an extension.
|
|
* The {cpp}20 additions and changes to `std::variant` have not yet been
|
|
implemented.
|
|
|
|
## Differences with Boost.Variant
|
|
|
|
This library is API compatible with `std::variant`. As such, its interface
|
|
is different from Boost.Variant's. For example, visitation is performed via
|
|
`visit` instead of `apply_visitor`.
|
|
|
|
Recursive variants are not supported.
|
|
|
|
Double storage is used instead of temporary heap backup. This `variant` is
|
|
always "stack-based", it never allocates, and never throws `bad_alloc` on
|
|
its own.
|