std::any vs. std::variant (1)

These two C++ types have a few things in common: they are both new additions to the C++17 Standard (requiring compiler support, so they are not back-portable), they allow initialization and (re-)assignment from different types (that is, they are in some ways true polymorphic types), and they can both throw exceptions when dereferenced incorrectly.

But they are very different under the hood. This article aims to be one of a planned mini-series covering how these two types are used so that, hopefully, you will know which, if either, to use.

Starting with any (which needs the <any> header file), it is possible to create a new variable without an explicit type specifier, such as:

any a1;                      // no initial value
any a2 = 1.5;                // contains a double
any a3{ "Hello, world!"s };  // string literal suffix, not const char*
vector<any> av{};            // vector of uninitialized any-type
a1 = true;                   // re-assignment

Member functions has_value() and type() can be used on an any object; the first of these returns false for an uninitialized any object, otherwise true, while the second can be used in comparison operations between two any objects as it returns a std::type_info object. (This object has a member function name() which returns a human-readable representation of the type.)

Moving onto variant (which needs the <variant> header file), the range of types which it can hold must be specified as template parameters. An uninitialized variant object takes the type of the first of these, which must again be default-constructible. The code below uses a using declaration for var (however, it’s not JavaScript!):

using var = variant<bool,double,string>;

var v1;                      // no initial value, will be bool
var v2 = 1.5;                // contains a double
var v3 { "Hello, world!"s }; // contains a string
vector<var> vv{};            // vector of variants, all bool initially
v1 = true;                   // re-assignment (to the same type)

If you require a std::variant to hold only non-default-constructible (user-defined) types, then there is the placeholder std::monostate that can be used as the first template parameter. The member function index() can be used on any variant object which returns the number (starting from zero) that the variant object holds, so v3.index() would here return 2. The (non-member) function template std::holds_alternative<T>(v) returns a boolean indicating whether the type T matches the variant object v specified within the parentheses.

You may well wonder how these types are implemented as their functionality differs greatly from the strict statically-typed language C++ has always been. Put simply, any is like a polymorphic abstract base class (ABC), while variant is like a C-style union (combined with an enum for the discriminant field). These implementation details are important when it comes to being able to usefully extract a value from an object of either of these types, which we’ll come to in the next article of this mini-series.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s