PrevUpHomeNext

Implementation Notes

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.

Questions and Answers

Q
Is strict_variant nothrow destructible?
A

Yes. Each contained item must be, and the variant is also.

Q

Is strict_variant nothrow copyable?

A

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.)

Q

Is strict_variant nothrow moveable? Is easy_variant?

A

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.

A2

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.

Q

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?

A2

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.

Q

When is strict_variant nothrow assignable? easy_variant?

A

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.

Q

When is strict_variant nothrow swappable? easy_variant?

A

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.


PrevUpHomeNext