Partners: KDAB Whole Tomato Software CppDepend

10 September 2018

How To Use std::visit With Multiple Variants

How To Use std::visit with multiple variants

std::visit is a powerful utility that allows you to call a function over a currently active type in std::variant. It does some magic to select the proper overload, and what’s more, it can support many variants at once.

Let’s have a look at a few examples of how to use this functionality.

The Amazing std::visit

Here’s a basic example with one variant:

struct Fluid { };
struct LightItem { };
struct HeavyItem { };
struct FragileItem { };

struct VisitPackage
{
    void operator()(Fluid& ) { cout << "fluid\n"; }
    void operator()(LightItem& ) { cout << "light item\n"; }
    void operator()(HeavyItem& ) { cout << "heavy item\n"; }
    void operator()(FragileItem& ) { cout << "fragile\n"; }
};

int main()
{
    std::variant<Fluid, LightItem, HeavyItem, FragileItem> package { 
        FragileItem() };
    std::visit(VisitPackage(), package);
}

Output:

fragile

We have a variant that represents a package with four various types, and then we use super-advanced VisitPackage structure to detect what’s inside. The example is also quite interesting as you can invoke a polymorphic operation over a set of classes that are not sharing the same base type.

Just a reminder - you can read the introduction to std::variant in my article: Everything You Need to Know About std::variant from C++17.

We can also use the “overload pattern” to use several separate lambda expressions:

template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
template<class... Ts> overload(Ts...) -> overload<Ts...>;

int main()
{
    std::variant<Fluid, LightItem, HeavyItem, FragileItem> package;

    std::visit(overload{
        [](Fluid& ) { cout << "fluid\n"; },
        [](LightItem& ) { cout << "light item\n"; },
        [](HeavyItem& ) { cout << "heavy item\n"; },
        [](FragileItem& ) { cout << "fragile\n"; }
    }, package);
}

In the above example, the code is much shorter, and there’s no need to declare a separate structure that holds operator() overloads.

Do you know what’s the expected output in the example above? What’s the default value of package?

Many Variants

But std::visit can accept more variants!

If you look at its spec it’s declared as:

template <class Visitor, class... Variants>
constexpr ReturnType visit(Visitor&& vis, Variants&&... vars);

and it calls std::invoke on all of the active types from the variants:

std::invoke(std::forward<Visitor>(vis), 
    std::get<is>(std::forward<Variants>(vars))...) 

// where `is...` is `vars.index()...`.

It returns the type from that selected overload.

For example we can call it on two packages:

std::variant<LightItem, HeavyItem> basicPackA;
std::variant<LightItem, HeavyItem> basicPackB;

std::visit(overload{
    [](LightItem&, LightItem& ) { cout << "2 light items\n"; },
    [](LightItem&, HeavyItem& ) { cout << "light & heavy items\n"; },
    [](HeavyItem&, LightItem& ) { cout << "heavy & light items\n"; },
    [](HeavyItem&, HeavyItem& ) { cout << "2 heavy items\n"; },
}, basicPackA, basicPackB);

The code will print:

2 light items

As you see you have to provide overloads for all of the combinations (N-cartesian product) of the possible types that can appear in a function.

Here’s a little diagram that shows this:

std::visit on two variants

If you have two variants - std::variant<A, B, C> abc and std::variant<X, Y, Z> xyz then you have to provide overloads that takes 9 possible configurations:

func(A, X);
func(A, Y);
func(A, Z);

func(B, X);
func(B, Y);
func(B, Z);

func(C, X);
func(C, Y);
func(C, Z);

In the next section, we’ll see how to leverage this functionality in an example that tries to match the item with a suitable package.

The Series

This article is part of my series about C++17 Library Utilities. Here’s the list of the topics in the series:

Resources about C++17 STL:

One Example

std::visit not only can take many variants, but also those variants might be of a different type.

To illustrate that functionality I came up with the following example:

Let’s say we have an item (fluid, heavy, light or something fragile) and we’d like to match it with a proper box (glass, cardboard, reinforced box, a box with amortization).

In C++17 with variants and std::visit we can try with the following implementation:

struct Fluid { };
struct LightItem { };
struct HeavyItem { };
struct FragileItem { };

struct GlassBox { };
struct CardboardBox { };
struct ReinforcedBox { };
struct AmortisedBox { };

variant<Fluid, LightItem, HeavyItem, FragileItem> item { 
    Fluid() };
variant<GlassBox, CardboardBox, ReinforcedBox, AmortisedBox> box { 
    CardboardBox() };

std::visit(overload{
    [](Fluid&, GlassBox& ) { 
        cout << "fluid in a glass box\n"; },
    [](Fluid&, auto ) { 
        cout << "warning! fluid in a wrong container!\n"; },
    [](LightItem&, CardboardBox& ) { 
        cout << "a light item in a cardboard box\n"; },
    [](LightItem&, auto ) { 
        cout << "a light item can be stored in any type of box, "
                "but cardboard is good enough\n"; },
    [](HeavyItem&, ReinforcedBox& ) { 
        cout << "a heavy item in a reinforced box\n"; },
    [](HeavyItem&, auto ) { 
        cout << "warning! a heavy item should be stored "
                "in a reinforced box\n"; },
    [](FragileItem&, AmortisedBox& ) { 
        cout << "fragile item in an amortised box\n"; },
    [](FragileItem&, auto ) { 
        cout << "warning! a fragile item should be stored "
                "in an amortised box\n"; },
}, item, box);

the code will output:

warning! fluid in a wrong container!

You can play with the code here @Coliru

We have four types of items and four types of boxes. We’d like to match the correct box with the item.

std::visit takes two variants item and box and then invokes a proper overload and shows if the types are compatible or not.
The types are very simple, but there’s no problem with extending them and adding features like wight, size or other important members.

In theory, we should write all combinations of overloads: it means 4*4 = 16 functions… but I used a trick to limit it. The code implements only 8 “valid” and “interesting” overloads.

So how you can “skip” such overload?

How to Skip Overloads in std::visit?

It appears that you can use the concept of generic lambda to implement a “default” overload function!

For example:

std::variant<int, float, char> v1 { 's' };
std::variant<int, float, char> v2 { 10 };

std::visit(overloaded{
        [](int a, int b) { },
        [](int a, float b) { },
        [](int a, char b) { },
        [](float a, int b) { },
        [](auto a, auto b) { }, // << default!
    }, v1, v2);

In the example above you can see that only four overloads have specific types - let’s say those are the “valid” (or “meaningful”) overloads. The rest is handled by generic lambda (available since C++14).

Generic lambda resolves to a template function. It has less priority than a “concrete” function overloads when the compiler creates the final overload resolution set.

BTW: I wrote about this technique in the recent update of my book.

If your visitor is implemented as a separate type, then you can use the full expansion of a generic lambda and use:

template <typename A, typename B>
auto operator()(A, B) { }

I think the pattern might be handy when you call std::visit on variants that lead to more than 5…7 or more overloads, and when some overloads repeat the code…

In our main example with items and boxes I use this technique also in a different form. For example

[](FragileItem&, auto ) { 
    cout << "warning! a fragile item should be stored "
            "in an amortised box\n"; },

The generic lambda will handle all overloads taking one concrete argument FragileItem and then the second argument is not “important”.

Summary

In this article, I’ve shown how you can use std::visit with many variants. Such a technique might lead to various “pattern matching” algorithms. You have a set of types, and you want to perform some algorithm based on the currently active types. It’s like doing polymorphic operations, but differently - as std::visit doesn’t use any v-tables.

Also, if you’d like to know how std::visit works underneath, then you might want to check out this post: Variant Visitation by Michael Park.

Have you used std::visit with many variants?
Can you share some examples?

Get my free ebook about C++17!

More than 50 pages about the new Language Standard.

C++17 in detail, by Bartlomiej Filipek

© 2017, Bartlomiej Filipek, Blogger platform
Any opinions expressed herein are in no way representative of those of my employers.
This site contains ads or referral links, which provide me with a commission. Thank you for your understanding.