PrevUpHomeNext

Basic Usage

The "basic usage" of strict_variant::variant mirrors that of boost::variant. If you are not familiar with boost::variant, I suggest you have a look at their tutorial first.

[Note] Note

In this example code, we will be implicitly using namespace strict_variant;.

I don't recommend doing this in your actual project, it's just for clarity in the tutorial.

A variant is a special kind of container that contains one value, which may be of several possible types. To create a variant, each of the types is passed as a template parameter. Any number of types may be specified, up to the implementation-specific limits on template complexity.

variant<int, std::string> v;

This code example declares a variant over the types int and std::string.

At all times, v will contain either an int or std::string. If v is default constructed, it will attempt to default construct the first type in the list. If that type cannot be default constructed, then variant is not default constructible either.

You can also initialize it using one of its values:

variant<int, std::string> u{"foo"};

Intuitively, you can think of v as a type-safe union. It has size and layout similar to the class:

class my_tagged_union;
  union {
    std::string s;
    int i;
  };
  int which;
};

where which is 0 or 1 depending on which member of the union is currently engaged. A variant is better than this because it is smart and handles the implementation details of which, when to call destructors, etc., for you.

You can change the value held by a variant simply by assigning to it:

v = 5;
v = 6;

v = "foo";
v = "bar";

The value can be recovered using the get function. get has syntax similar to a pointer cast -- it takes a pointer to a variant, and as a template parameter, the desired type. It then returns a pointer to the type, which is nullptr if that was not the type of the variant.

v = 5;
v = 6;
assert(get<int>(&v));
assert(!get<std::string>(&v));
assert(6 == *get<int>(&v));

v = "foo";
v = "bar";
assert(get<std::string>(&v));
assert(!get<int>(&v));
assert("bar" == *get<std::string>(&v));
[Note] Note

In boost::variant docs, you will also see a version of get which takes a reference rather than a pointer, and returns a reference. E.g.

int & foo = get<int>(v);

This throws a bad_variant_access exception if v does not currently have type int.

strict_variant doesn't have that built-in, but it's trivial to implement it yourself if you want it.

Visitors

get is fine for small cases, but for larger / more sophisticated cases, a better way to access a variant is to use apply_visitor.

A visitor is a "callable" C++ object, which can be called with any of the types contained in the variant as a parameter.

Here's an example of a function object, which maps an int or a string to a string.

struct formatter {
  std::string operator()(const std::string & s) const { return s; }
  std::string operator()(int i) const { return "[" + std::to_string(i) + "]"; }
};

When calling apply_visitor, the visitor comes first, and the variant second: apply_visitor returns whatever the visitor returns.

v = 5;
assert("[5]" == apply_visitor(formatter{}, v));

v = "baz";
assert("baz" == apply_visitor(formatter{}, v));

One of the selling points of apply_visitor as compared to get is that it allows you to turn a class of runtime errors into compile-time errors.

Suppose that you are writing an application using a variant, and you realize that you need to add another type to the variant, having already written much code.

If your code looks like this:

std::string
format_variant(const variant<std::string, int> & v) {
  if (const auto * i = get<int>(&v)) {
    return "[" + std::to_string(*i) + "]";
  } else if (const auto * str = get<std::string>(&v)) {
    return *str;
  } else {
    assert(false && "Unsupported type!");
  }
}

then when you add a new type, any functions that you didn't update will fail, but only at runtime, by throwing exceptions or similar.

When you use apply_visitor style, if a new type is added that formatter cannot handle, a compile-time error will result. So the compiler will help you find any code locations that you didn't update after the new type is added.

Recursive Variants

A very handy use of variants has to do with creating recursive data structures. This crops up commonly in parsing problems, for instance when representing an abstract syntax tree.

For instance, suppose I want to represent an XML tree. I could start like this:

using xml_attribute = std::pair<std::string, std::string>;

and try to declare a node like this:

struct xml_node {
  std::string name;
  std::vector<xml_attribute> attributes;
  std::vector<variant<std::string, xml_node>> body; // ERROR
};

The trouble with this is that xml_node is incomplete at the time that it is used in the class body of xml_node, so it won't actually work. (variant really needs to know the size of anything that it is supposed to contain.)

The class template recursive_wrapper<T> can be used to surmount this difficulty. The argument to recursive_wrapper can be an incomplete type and it can still be used in a variant without a problem. (recursive_wrapper works by allocating the object on the heap instead -- it only actually contains a pointer.)

using xml_attribute = std::pair<std::string, std::string>;
struct xml_node;

using xml_variant = variant<std::string, recursive_wrapper<xml_node>>;

struct xml_node {
  std::string name;
  std::vector<xml_attribute> attributes;
  std::vector<xml_variant> body;
};

See also boost::spirit tutorial for an extended example using boost::variant.

recursive_wrapper<T> is, from the user's point of view, the same in strict_variant as it is in boost::variant.

There are several ways to define an xml-tree data structure like this -- you can have a look also at boost::property_tree for instance. But using a variant like this is surely one of the simplest, most lightweight, and extensible solutions.

[Note] Note

We don't provide any analogue of boost::make_recursive_variant.

emplace

Another way to assign a value to a variant is to use the emplace function.

v.emplace<int>(5);
v.emplace<int>(6);

There are a few reasons to use emplace which are outside the scope of the basic tutorial. But a few of them are:

  1. If your type is neither moveable nor copyable, you cannot use assignment but you may be able to use emplace.
  2. If assignment would be ambiguous, you can use emplace to explicitly select the type that you want to put in the container.
Conclusion

That should be enough to get you moving for most uses of variant. If you want to see more advanced uses of variant, using lambda function visitors, etc., see the Advanced Usage page.

As a final remark, let me suggest another way of looking at variant.

Ostensibly, variant is just a standard container, like vector, set, or optional.

But another way of thinking about variant is that it is a simple and efficient way of achieving runtime polymorphism.

Every student of C++ learns about inheritance, virtual inheritance, virtual dispatch, and so on. And about how to achieve runtime polymorphism by creating an interface base class, and making multiple classes that derive from / implement the interface.

Another way to get runtime polymorphism is, simply use a variant where you would have used the base class, and use visitors to get different behavior based on the runtime type.

Using a variant is attractive because:

  1. Inheritance creates tight-coupling -- a change to one of the objects may necessitate a change in the interface, and then to all of the objects. Objects held in a variant don't need to be related at all.
  2. Usually, inheritance leads to a design where you manipulate pointers to the base class, and end up allocating everything on the heap. With variant, the objects can be allocated on the stack, which can make a huge performance difference in some cases.
  3. Dispatching a visitor to a variant is very simple and transparent to the compiler. When the compiler encounters apply_visitor, it knows all of the different types that the variant could be holding, and it just has to select the right function to call based on the which value. With virtual dispatch, the compiler often can't actually know all the possible types until every compilation unit has been compiled, because any compilation unit can declare a new class which inherits from the base class. How virtual dispatch is implemented may vary from compiler to compiler, but typically it involves indirection through a "vptr" associated to each instance, which points to a table of function pointers called a "vtable" associated to each class. And we usually can't know all the possible vptr values until we've seen every compilation unit. So virtual dispatch is usually opaque to the optimizer. With a variant, the whole dispatch is transparent and can potentially be inlined. This can sometimes be alleviated by link-time optimizations and devirtualization optimizations, but not always, and at time of writing, not typically either.
  4. Variants can be used even in very restrictive software environments. Virtual dispatch ultimately relies on C++ vtables and RTTI. A variant will work even in a project which is compiled with -fno-rtti. Essentially, the int which value plays the role that the vptr and RTTI play.
  5. Sometimes, the same visitor can be used with multiple different variant types. This allows code reuse in cases where with an inheritance-based solution you might have needed to duplicate code.

Sometimes inheritance is really what you want and need, but in other cases, variant is a good alternative, and a good tool for your modern C++ toolkit.


PrevUpHomeNext