Last Update:
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:

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:
- Refactoring with
std::optional - Using
std::optional - Error handling and
std::optional - About
std::variant - About
std::any - In place construction for
std::optional,std::variantandstd::any std::string_viewPerformance and followup and another post about string initialization- C++17 string searchers and followup
- Conversion utilities
- Working with
std::filesystem - Something more?
Resources about C++17 STL:
- C++17 In Detail by Bartek!
- C++17 - The Complete Guide by Nicolai Josuttis
- C++ Fundamentals Including C++ 17 by Kate Gregory
- Practical C++14 and C++17 Features - by Giovanni Dicanio
- C++17 STL Cookbook by Jacek Galowicz
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?
