How to Use std::visit With Multiple Variants
In this article, we'll show how you can use std::visit with many variants, a technique which might lead to various 'pattern matching' algorithms.
Join the DZone community and get the full member experience.
Join For Freestd::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 << "fraile\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 << "fraile\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.
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 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 an overload?
How to Skip Overloads in std::visit
It appears that you can use the concept of generic lambdas 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 lambdas (available since C++14).
A generic lambda resolves to a template function. It has less priority than "concrete" function overloads when the compiler creates the final overload resolution set.
By the way, 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 also use this technique 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?
More from the Author:
Bartek recently published a book - "C++17 In Detail"- rather than reading the papers and C++ specification drafts, you can use this book to learn the new Standard in an efficient and practical way.
Published at DZone with permission of Bartłomiej Filipek, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments