Table of Contents

std::visit from C++17 is a powerful utility that allows you to call a function over a currently active type in std::variant.

In this post, I’ll show you how to leverage all capabilities of this handy function: the basics, applying on multiple variants, and passing additional parameters to the matching function.

Let’s dive right in.

The Amazing std::visit  

Here’s a basic example with one variant:

#include <iostream>
#include <variant>

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

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

int main() {
    std::variant<Fluid, LightItem, HeavyItem, FragileItem> package { 
        FragileItem()
    };
    
    // match with the current state in "package"
    std::visit(VisitPackage(), package);
}

Output:

fragile

Play with code @Compiler Explorer

We have a variant (std::variant) that represents a package with four various types, and then we use the VisitPackage structure to detect what’s inside.

Just a reminder - you can read the introduction to std::variant in my articles:

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

#include <iostream>
#include <variant>

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

template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
template<class... Ts> overload(Ts...) -> overload<Ts...>; // line not needed in C++20...

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

    std::visit(overload{
        [](Fluid& )       { std::cout << "fluid\n"; },
        [](LightItem& )   { std::cout << "light item\n"; },
        [](HeavyItem& )   { std::cout << "heavy item\n"; },
        [](FragileItem& ) { std::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.

See code @Compiler Explorer

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 diagram that illustrates this functionality:

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 on C++17  

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 an appropriate 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 @Compiler Explorer

We have four types of items and four types of boxes. We want 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 extending them and adding features like weight, size, or other important members.

In theory, we should write all overload combinations: 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 can you “skip” such overload?

How to Skip Overloads in std::visit?  

It appears that you can use the concept of a 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 overload when the compiler creates the final overload resolution set.

BTW: I wrote about this technique in my book on C++17.

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) { }

C++20 update:

This would also correspond to the following C++20 code leveraging abbreviated function templates syntax:

auto operator()(auto A, auto 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 primary 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.”

Bonus: how to pass parameters?  

There’s also one trick I’d like to share with you today.

What if you’d like to pass some additional params to the matching function?

in theory:

// pass 10 to the overload?
std::visit(/*some visitor*/, myVariant, /*your param*/10);

The first option - a variant of one object?  

Passing 10 won’t work for std::visit (do you know why?), so why not wrap it into a separate variant of only one type?

std::variant<Fluid, GlassBox> packet;
std::variant<int> intParam { 200 };

std::visit(overload{
    [](Fluid&, int v) { 
        std::cout << "fluid + " << v << '\n';            
    },
    [](GlassBox&, int v) { 
        std::cout << "glass box + " << v << '\n';            
    }
}, packet, intParam);

Play with code @Compiler Explorer

It works perfectly fine!

With this approach, we pay for additional storage needed in variant, but still, it’s not too bad.

The second option - a custom functor  

How about another option:

Let’s write two functions:

void checkParam(const Fluid& item, int p) {
    std::cout << "fluid + int " << p << '\n';
}

void checkParam(const GlassBox& item, int p) {
    std::cout << "glass box + int " << p << '\n';
}

Let’s try to implement support for those two.

What we can do here is to write a custom visitor functor object that would wrap the parameter as a data member:

struct VisitorAndParam {
    VisitorAndParam(int p) : val_(p) { }

    void operator()(Fluid& fl) { checkParam(fl, val_); }
    void operator()(GlassBox& glass) { checkParam(glass, val_); }

    int val_ { 0 };
};

Now we can call it as follows:

int par = 100;
std::visit(VisitorAndParam{par}, packet);

As you can see, our visitor is a “proxy” to call the matching function.

Since the call operator is relative simple and duplicated, we can make it a template function:

// C++20:
void operator()(auto& item) { checkParam(item, val_); }

// C++17:
template <typename T>
void operator()(T& item) { checkParam(item, val_); }

Play with code @Compiler Explorer

The third option - with a lambda  

Since we can use a functor object, then a similar thing can be done with a lambda!

What we can do is we can write a generic lambda that captures the parameter.

And now we can try std::visit with the following code:

int param = 10;
std::visit(overload{
    [&param](const auto& item) {  
        checkParam(item, param);
    },
}, packet);

Cool Right?

And we can try wrapping this code into a separate helper function:

void applyParam(const auto& var, auto param) {
    std::visit(overload{
        [&param](const auto& item) {  
            checkParam(item, param);
        },
    }, var);
}

Play with code @Compiler Explorer

I noticed it during the read of this great book (“Software Architecture with C++” by Adrian Ostrowski and Piotr Gaczkowski), and it was used for implementing state machines.

(We’ll talk about FSM in some future blog posts :))

Would you like to see more?
I explored Finite State Machines with std::variant. See the first or the second article, which are available for C++ Stories Premium/Patreon members. See all Premium benefits here.

Summary  

In this article, I’ve shown how you can use std::visit with multiple 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, you might want to check out this post: Variant Visitation by Michael Park.

Back to you:

  • Do you use std::variant? Do you use it with std::visit or custom alternatives?

Share your thoughts in the comments below the article.