PrevUpHomeNext

Advanced Usage

Emplace constructor

In some cases, you might want to use emplace to initialize a variant to a specific type explicitly. For instance it might be in an initializer list, and it's not an option to use the emplace method.

The emplace constructor is selected using tag dispatch:

variant::variant(emplace_tag<T>, args...)

for example, in an initializer list, it looks like this:

my_struct::my_struct()
  : v(emplace_tag<std::string>{}, "foo") {}

Generalizing constructor

A variant can be converted to a variant over strictly more types. This is called the "generalizing" constructor. This was also supported by boost::variant.

variant<int, double> v;
variant<int, double, std::string> v2{v};

This constructor also allows reordering the types, and adding or removing recursive_wrapper.

variant<int, double> v;
variant<double, int> v2{v};

variant<recursive_wrapper<int>, double> v3;
v3 = v;
v2 = v3;
v = v2;

More general visitors

One thing which you'll note in boost::variant docs is that in boost::variant, a visitor should always be a structure and should derive from boost::static_visitor, or, failing that, explicitly define a typedef result_type which matches the return type of the function object.

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

In strict_variant, this isn't necessary, the return type will be deduced. So you can omit static_visitor.

This is necessary to support lambdas being used as visitors, for instance.

Lambda visitors

You can also use a lambda as a visitor in some cases:

strict_variant::variant<int, float> v;
v = 5;

// Prints 10
std::cout << strict_variant::apply_visitor([](double d) -> double { return d * 2; }, v)
          << std::endl;

v = 7.5f;

// Prints 22.5
std::cout << strict_variant::apply_visitor([](double d) -> double { return d * 3; }, v)
          << std::endl;
Generic visitors

One of the nicest things about visitors is that they can use templates -- when you have a visitor with many types which should be handled similarly, this can save you much typing. It also means that a single visitor can often be equally useful for many different variants, which helps you to be very DRY.

struct formatter {
  std::string operator()(const std::string & s) const { return s; }

  template <typename T, typename = typename std::enable_if<std::is_arithmetic<T>::value>::type>
  std::string operator()(T t) const {
    return "[" + std::to_string(t) + "]";
  }

  template <typename T>
  std::string operator()(const std::vector<T> & vec) const {
    std::string result = "{ ";
    for (unsigned int i = 0; i < vec.size(); ++i) {
      if (i) { result += ", "; }
      result += (*this)(vec[i]);
    }
    result += " }";
    return result;
  }
};

This generic visitor is appropriate for a wide variety of variants:

variant<int, double> v1;
variant<float, std::string, std::vector<int>> v2;
variant<long, std::vector<std::vector<std::string>>> v3;

apply_visitor(formatter{}, v1);
apply_visitor(formatter{}, v2);
apply_visitor(formatter{}, v3);
Polymorphic return type

A more elaborate use of the deduced return-type feature involves function objects that might return multiple different types depending on the arguments.

TODO

This works as long as the returns can be reconciled in a fashion similar to std::common_type. [12]

One feature of boost::variant which we don't currently support is delayed visitation. For this, you will have to use a capturing lambda or some other sort of ad-hoc function object.

Other interesting visitor styles
apply_visitor( overload( [](double d) -> std::string { return std::to_string(d); },
                         [](std::string s) -> std::string { return s; }
                       ),
               v1 );

Multivisitation

strict_variant has full support for visiting 2 or more variants simultaneously, just as boost::variant does.

However for this you must include <strict_variant/multivisit.hpp>.

Throwing Assignment

Just like boost::variant and any other standard container, strict_variant can be moved, copied and assigned.

However, like boost::variant it has some restrictions.

strict_variant also requires that its types are nothrow_destructible. It doesn't require that they are copyable or moveable.

However, strict_variant is different in that it is only assignable when each type is assignable AND each type is nothrow_move_constructible. If this doesn't happen, compiling an assignment operation will fail with a static_assert.

Since you can't always make your types nothrow_move_constructible, there are a few handy solutions.

Suppose that our goal is code like this:

// A class with a throwing move
class X {
  std::string foo;
  std::string bar;

public:
  X() noexcept;
  X(const X &) noexcept(false);
  X & operator=(const X &) noexcept(false);
};

// A variant containing X
void
goal() {
  variant<int, X> v;
  v = 5;
  v = X();  // Error: No assignment if X has a throwing move!
}

This code will fail to compile, because v cannot be assigned if X has a throwing move. We have a few possible remedies:

  1. Use a recursive_wrapper.

    void
    goal() {
      variant<int, recursive_wrapper<X>> v;
      v = 5;
      v = X();
    }
    
  2. Use easy_variant instead. It's the same, but it implicitly applies wrappers to value types with a throwing move.

    void
    goal() {
      easy_variant<int, X> v;
      v = 5;
      v = X();
    }
    
  3. Use emplace instead of assignment. This works even if X is not copyable or moveable.

    void
    goal() {
      variant<int, X> v;
      v.emplace<int>(5);
      v.emplace<X>();
    }
    


[12] Note that we don't actually use std::common_type: for one, that is C++14, for two, it wouldn't allow us to return references. We compromise by allowing the return of lvalue references but not rvalue references -- non-lvalue references will decay when returned by apply visitor.


PrevUpHomeNext