PrevUpHomeNext

Abstract and Motivation

Abstract

strict_variant is a safe and performant discriminated union container, in the style of boost::variant, designed for the C++11 standard.

Motivation

strict_variant is similar in design to boost::variant, however, there are a few important changes to the interface and the implementation. The basic goal is to make it more convenient for the programmer to use -- debatably there are some efficiency gains in some situations, but that's not the primary goal.

Interface

Both boost::variant and std::variant support construction from any instance of one of the value types, or any type convertible to one of the value types. In boost::variant, even C++03 is supported, so in that case there actually is one constructor for each value type. In C++17 std::variant, it is specified that when constructing a variant from a value, the value-type / conversion which will be used is determined by overload resolution, essentially the same mechanism.

However, this behavior is somewhat inconvenient in some scenarios. For instance, this code compiles just fine, though we might not want it to:

boost::variant<std::string, int> v;

v = true; // bool -> int conversion is selected

and this code fails because of ambiguous overload resolution:

boost::variant<bool, long, unsigned long long, std::string> v;

v = 100; // int -> long and int -> ull are equally good conversions

Another, slightly different example. This code does what you expect:

boost::variant<int, std::string> v;

v = "The future is now!";
std::cout << v.which() << std::endl;
std::cout << v << std::endl;

When tested with boost 1.58 and gcc 5.4.0, it outputs:

1
The future is now!

However, when we change it slightly, it does something totally different:

boost::variant<bool, std::string> v;

v = "The future is now!";
std::cout << v.which() << std::endl;
std::cout << v << std::endl;

It now selects bool, because of implicit pointer-to-bool conversions:

0
1

strict_variant addresses such issues by modifying the overload resolution process. Before overload resolution takes place, some candidates are excluded:

For a prime use-case, you might be using the variant to represent value types when binding C++ code to some scripting language implementation. Many scripting languages support bool, int, float, std::string, etc. as primitive values, but basically overload resolution is most likely to become ambiguous in situations like this. We refine the overload resolution process on the assumption that what you are trying to do is store the value as faithfully as possible for later recovery, while still permitting portable integer promotions and such.

For a complete description of the conversion rules, check out TODO LINK.

Implementation

The second way in which strict_variant differs from boost::variant is the implementation approach -- how exactly is the storage managed, and how is the never-empty guarantee achieved.

Much digital ink has been spilled regarding the technical difficulties of implementing a general, never-empty variant in C++. (For a long list of references, see the footnotes of the design section.)

At least, it's clear that there is no "perfect" solution and there are instead many possible compromises, most of which are incomparable.

In the design section we'll discuss various options in detail, but to motivate our approach, we'd like to argue that an approach which requires as little bandwidth to explain to the programmer as possible is a good one.

To that end, let's try to design a general variant from the ground up by reducing the problem to a simpler one.

The simpler problem is,

It turns out that this problem is really much simpler -- there is no longer any issue with the "throwing, type-changing assignment". We can always vacate the storage and then move the new value in, since the move won't throw. Compared to the general problem, this kind of variant is straightforward to implement optimally. Basically everything can be done by "copy and swap" and similar idioms -- there are no "surprises" here really.

Now,

It turns out we can do it in a simple way using a little template:

template <typename T>
using wrap_if_throwing_move_t = typename std::conditional<std::is_nothrow_move_constructible<T>::value,
                                                          T,
                                                          recursive_wrapper<T>>::type;

(The reader will hopefully recall recursive_wrapper from boost::variant.

A recursive_wrapper<T> represents a heap-allocated instance of T. It can be used to declare a variant which morally contains a T even if T is an incomplete type.)

Now, for the general form of the variant, we simply apply wrap_if_throwing_move_t to each value-type, and defer to the simpler implementation.

(In the strict_variant library, the template class strict_variant::variant is the "easy case" solution, and the template alias strict_variant::easy_variant is the general-case solution.)

The point is that even if T has a throwing move, recursive_wrapper<T> can always be moved into storage without throwing, because it is just a pointer.

Some care must be taken -- throughout the variant interface, it is important that recursive_wrapper is transparently pierced for the user, not only for convenience now, but for correctness -- the user should not be able to make the recursive_wrapper empty.

But, with adequate care for such issues, you are now perfectly capable of going and writing your own strict_variant, and you'll get the same thing at least in regards to all the "controversial" parts of implementing a variant.

In fact, you could probably stop reading the documentation right now and go and use the variant, and hopefully wouldn't ever be surprised by it's behavior given what I just told you.

(This is not a trick -- the rest of the documentation does actually exist and is accurate to the best of my knowledge if you want to read it :p)

Example

To illustrate the differences between the strict_variant and boost::variant implementations, consider the following example:

using var_t = boost::variant<A, B>;

var_t v{A()};
std::cout << "1" << std::endl;
v = B();
std::cout << "2" << std::endl;
v = A();
std::cout << "3" << std::endl;

where A and B have been configured to log all ctor and dtor calls.

boost::variant generates the following output:

A()
A(A&&)
~A()
1
B()
B(B&&)
A(const A &)
~A()
B(const B &)
~A()
~B()
~B()
2
A()
A(A&&)
B(const B &)
~B()
A(const A &)
~B()
~A()
~A()
3
~A()

while strict_variant generates

A()
A(A&&)
~A()
1
B()
B(B&&)
~A()
~B()
2
A()
A(A&&)
~B()
~A()
3
~A()

For a programmer, this means the following:

But, these issues aside, I argue that strict_variant adds value because the programmer can have a much simpler mental model of how it works and what exactly can happen when they manipulate it.

For an extended comparison of this and several other possible implementation strategies, see the design page.

Goals

The design goals of strict_variant are:


PrevUpHomeNext