As mentioned in the design
notes, the implementation details of the actual variant
class end up being relatively simple, and the wrapper abstraction ends up
doing much of the work related to exception safety.
The main subtlety involved in the overall implementation actually has to
do with the recursive_wrapper
.
If you'll recall, recursive_wrapper<T>
represents a heap-allocated instance of T
.
In the design section, we pointed out that a recursive_wrapper<T>
could be moved by simply moving a pointer.
However, those who are intimately familiar with implementation details of
boost::variant
may recall that that is not actually
how boost::recursive_wrapper
works.
In boost::variant
, when a recursive_wrapper<T>
is move-constructed, what happens is a new dynamic allocation is made, and
T
is move-constructed there.
We'll call that the "value-move", in contrast with the "pointer-move".
Clearly, the pointer-move plays a crucial role in strict_variant
,
since the value-move is throwing, while the pointer-move isn't.
But, we can't always use the pointer-move.
To see why, consider that the point of the whole exercise is to create a
never-empty variant. What happens if variant x
is moved-assigned into variant y
?
If x
contains a recursive_wrapper
and this is pointer-moved
into y
, then x
is left holding nothing. It's effectively
in an empty state, and since visitation implicitly pierces the recursive_wrapper
, the result would be
UB if you attempt to visit x
after the move. Nevermind that "it's not empty, it merely contains an
empty container" -- that's the sort of explanation that only a compiler
would be satisfied with.
(Granted, in some applications, no one plans to visit a variant that has been moved from, and if they could avoid a dynamic allocation, they would prefer the pointer-move even if the variant gets left in a precarious state. We might implement some sort of configuration option to allow for this optimization, but doesn't exist now and I don't think it should be the default.)
With this in mind, how should strict_variant::recursive_wrapper
's
move ctor actually be implemented? Should we use tag-dispatch to differentiate
the two options?
The strategy that we've adopted is, strict_variant::recursive_wrapper
's
move ctor is the pointer-move. But in most cases, when visiting a variant,
even for purposes of copy or move construction of another variant, we call
a function detail::pierce_recursive_wrapper
which ensures
that we pierce the wrapper and move the value rather than the wrapper.
namespace detail { /*** * Function to pierce the recursive_wrapper template */ template <typename T> inline auto pierce_recursive_wrapper(T && t) -> T { return std::forward<T>(t); } template <typename T> inline T & pierce_recursive_wrapper(recursive_wrapper<T> & t) { return t.get(); } template <typename T> inline T && pierce_recursive_wrapper(recursive_wrapper<T> && t) { return std::move(t.get()); } template <typename T> inline const T & pierce_recursive_wrapper(const recursive_wrapper<T> & t) { return t.get(); } } // end namespace detail
Whenever a user-defined visitor is applied, we use this function to transparently remove the wrapper for them.
We also use that for almost all of the internal visitors,
which are used to implement move construction, assignment, etc. The only
times that we want to visit a variant
and not pierce the wrapper, are
(Obviously, when constructing the value that gets moved into our storage though, we have to use exactly the type that was specified in the template parameters. This just won't always be the same as what the visited value is, even when copy or move constructing.)
If we move after piercing the wrapper, that performs a value move, and when we want to perform a pointer move, we move without piercing the wrapper.
This ensures correctness also when we are using the "generalizing"
ctors of variant
.
See the move_constructor
and move_assigner
visitors
in variant.hpp
for complete examples.
strict_variant
nothrow
destructible?
Yes. Each contained item must be, and the variant
is also.
Is strict_variant
nothrow
copyable?
If each contained type is nothrow copyable, then strict_variant
is also. Otherwise, no. (If some types are not copyable, then strict_variant
isn't copyable at
all.)
Is strict_variant
nothrow
moveable? Is easy_variant
?
If each contained type is nothrow moveable, then strict_variant
is also. Since easy_variant
puts each type with a throwing move inside a recursive_wrapper
which is nothrow moveable, easy_variant
is always nothrow moveable.
WRONG.
Even though recursive_wrapper<T>
is nothrow moveable, that refers
to the pointer-move, not the value-move. And value-move is what strict_variant
must use in its move
constructor, see above. value-move is always potentially
throwing no matter what T
is, if only because of the dynamic allocation. The pointer-move is
only okay to use when moving the temporary into storage.
strict_variant
is nothrow
moveable as long as all the constituent types are, and as long as none
of them are recursive_wrapper<T>
. Otherwise, its move constructor
is potentially throwing.
This sucks! I am working hard to make my types nothrow moveable so
I can be on the fast-track for strict_variant
,
but then strict_variant
turns around and gives me a throwing move when I don't succeed -- even
when I use easy_variant
like instructed!
This means that actually, in that
code from the tutorial, where we represent an XML node using
recursive_wrapper
,
we're actually on the slow track in that vector
,
aren't we? So we'll implicitly get tons of copies of child xml nodes
when parsing an xml tree and the vector has to resize, won't we?
Yes. I agree that this is unfortunate.
But, all I can say is that the implementation started with a pretty simple design idea, and I feel I was driven inexorably to this conclusion.
Somehow, the whole point of recursive_wrapper
is to provide some syntactic sugar for the user. When it is really
necessary or makes more sense for the something to be on the heap,
but we don't want the user to have to think about that fact, recursive_wrapper
is a good solution,
because it gets it on the heap and gets transparently pierced by the
variant
interface.
But if the recursive_wrapper
has an empty state, which the user can easily get it into, this means
that the user can transparently get UB, which is not a good thing.
If it is important to you that you wrap up a type with a throwing move
in a way that doesn't give strict_variant
a throwing move, then what I recommend is that instead of using recursive_wrapper<T>
,
you use std::unique_ptr<T>
.
Then when assigning, use std::make_unique<T>
, and when visiting, you get unique_ptr<T>
instead of T
, and you
just *
it yourself, or
do whatever handling you want for the null state. That seems more sound
than the variant
itself
*
'ing it for you when
it could legitimately be null.
In this case, the variant will be nothrow moveable, because it will be using the pointer-move effectively.
The main drawback at that point is that, unique_ptr
is not copyable, so your strict_variant
won't be copyable. But it's pretty simple to roll your own quick unique_ptr
that rectifies that. Who
knows, we might even do that for you in the next release...
Hopefully, this at least convinces you that strict_variant
is a flexible and useful low-level component, and that you can easily
configure it like this to get the semantics that you need without introducing
unnecessary overhead.
When is strict_variant
nothrow assignable? easy_variant
?
strict_variant
is copy
assignable when all of its types are assignable, copy constructible,
and nothrow move constructible. It is noexcept
copy assignable if each type's copy assignment and copy construction
is noexcept
. easy_variant
is assignable if each
type is copy constructible and assignable. Since copying a recursive_wrapper
involves a dynamic
allocation, easy_variant
's
copy assignment can throw if recursive_wrapper
gets involved.
strict_variant
is move
assignable when all of its types are move assignable and nothrow move
constructible. It is noexcept
move assignable if each type's move assignment is noexcept
,
AND if none is recursive_wrapper
.
(Similar to when strict_variant
has a throwing move.) Because, we have to use the value-move first,
and can only use the pointer-move to move into storage.
easy_variant
is move
assignable as long as every template parameter is move assignable and
move constructible. It is nothrow move assignable as long as all of
the types are nothrow move assignable and nothrow move constructible
-- none of them should actually end up in a recursive_wrapper
.
When is strict_variant
nothrow swappable? easy_variant
?
strict_variant
is nothrow
swappable whenever all of its members are nothrow move constructible.
easy_variant
is always
nothrow swappable.
This is because when we swap, it's okay to use the pointer move, since
it can't fail and won't produce an empty variant. As a result, swapping
may be faster than move-assigning when recursive_wrapper
is involved, because it will avoid a dynamic allocation. If your variant
contains a type which is
default-constructible and has no external side-effects, it may be preferable
as a micro-optimization to default-construct a variant in that state
and then call swap
,
rather than move-assigning or move-constructing.