Last Update:
How To Use std::visit With Multiple Variants and Parameters
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:
- Everything You Need to Know About std::variant from C++17.
- and additionally about polymorphism: Runtime Polymorphism with std::variant and std::visit
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:
- Refactoring with
std::optional
- Using
std::optional
- Error handling and
std::optional
- Everything You Need to Know About
std::variant
from C++17 - Everything You Need to Know About
std::any
from C++17 std::string_view
Performance and followup- C++17 string searchers and followup
- Conversion utilities: on
std::from_chars
- from a string to a number and onstd::to_chars
- from numbers into strings - How to get File Size in C++? and std:filesystem::file_size Advantages and Differences
- How To Iterate Through Directories
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 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{
[¶m](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{
[¶m](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 withstd::visit
or custom alternatives?
Share your thoughts in the comments below the article.
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: