Visiting a std::variant with the Overload Pattern
Typically, you use the overload pattern for a std::variant
. std::variant
is a type-safe union. A
std::variant
(C++17) has one value from one of its types. std::visit
allows you to apply a visitor to it. Exactly here comes the overload pattern convenient into play.
In my last post, “Smart Tricks with Parameter Packs and Fold Expressions“, I introduced the overload pattern as a smart trick to create an overload set using lambdas. Typically, the overload pattern is used for visiting the value held by a std::variant
.
I know from my C++ seminars that most developers don’t know std::variant
and std::visit
and still, use a union. Therefore, let me give you a quick reminder about std::variant
and std::visit
.
std::variant (C++17)
A std::variant is a type-safe union. An instance of std::variant has a value from one of its types. The value must not be a reference, C-array, or void. A std::variant can have one type more than once. A default-initialized std::variant will be initialized with its first type. In this case, the first type must have a default constructor. Here is an example based on cppreference.com.
// variant.cpp #include <variant> #include <string> int main(){ std::variant<int, float> v, w; v = 12; // (1) int i = std::get<int>(v); w = std::get<int>(v); // (2) w = std::get<0>(v); // (3) w = v; // (4) // std::get<double>(v); // (5) ERROR // std::get<3>(v); // (6) ERROR try{ std::get<float>(w); // (7) } catch (std::bad_variant_access&) {} std::variant<std::string> v("abc"); // (8) v = "def"; // (9) }
I define both variants v and w. They can have an int and a float value. Their initial value is 0. v becomes 12 (line 1). std::get<int>(v) returns the value. In lines (2) – (3), you see three possibilities to assign variant v the variant w. But you have to keep a few rules in mind. You can ask for the value of a variant by type (line 5) or index (line 6). The type must be unique and the index valid. On line 7, the variant w holds an int value. Therefore, I get a std::bad_variant_access exception. If the constructor or assignment call is unambiguous, a simple conversion occurs. That is the reason that it’s possible to construct a std::variant<std::string> in line (8) with a C-string or assign a new C-string to the variant (line 9).
Of course, there is way more about std::variant.
Read the post “Everything You Need to Know About std::variant from C++17” by Bartlomiej Filipek.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
Thanks to the function std::visit
, C++17 provides a convenient way to visit the elements of a std::variant
.
std::visit
According to the classical design patterns, what sounds like the visitor pattern is a kind of visitor for a container of variants.
std::visit allows you to apply a visitor to a container of variants. The visitor must be callable. A callable is something that you can invoke. Typical callables are functions, function objects, or lambdas. I use lambdas in my example.
// visitVariants.cpp #include <iostream> #include <vector> #include <typeinfo> #include <variant> int main(){ std::cout << '\n'; std::vector<std::variant<char, long, float, int, double, long long>> // 1 vecVariant = {5, '2', 5.4, 100ll, 2011l, 3.5f, 2017}; for (auto& v: vecVariant){ std::visit([](auto arg){std::cout << arg << " ";}, v); // 2 } std::cout << '\n'; for (auto& v: vecVariant){ std::visit([](auto arg){std::cout << typeid(arg).name() << " ";}, v); // 3 } std::cout << "\n\n"; }
I create in (1) a std::vector of variants and initialize each variant. Each variant can hold a char, long, float, int, double, or long long value. It’s pretty easy to traverse the vector of variants and apply the lambda (lines (2) and (3) to it. First, I display the current value (2), and second, thanks to the call typeid(arg).name() (3), I get a string representation of the type of the current value.
Fine? No!. I used it in the program visitVariant.cpp
generic lambda. Consequently, the string representations of the types are pretty unreadable using GCC: “i c d x l f i
“. Honestly, I want to apply a specific lambda to each type of the variant. Now, the overload pattern comes to my rescue.
Overload Pattern
Thanks to the overload pattern, I can display each type with a readable string and display each value in an appropriate way.
// visitVariantsOverloadPattern.cpp #include <iostream> #include <vector> #include <typeinfo> #include <variant> #include <string> template<typename ... Ts> // (7) struct Overload : Ts ... { using Ts::operator() ...; }; template<class... Ts> Overload(Ts...) -> Overload<Ts...>; int main(){ std::cout << '\n'; std::vector<std::variant<char, long, float, int, double, long long>> // (1) vecVariant = {5, '2', 5.4, 100ll, 2011l, 3.5f, 2017}; auto TypeOfIntegral = Overload { // (2) [](char) { return "char"; }, [](int) { return "int"; }, [](unsigned int) { return "unsigned int"; }, [](long int) { return "long int"; }, [](long long int) { return "long long int"; }, [](auto) { return "unknown type"; }, }; for (auto v : vecVariant) { // (3) std::cout << std::visit(TypeOfIntegral, v) << '\n'; } std::cout << '\n'; std::vector<std::variant<std::vector<int>, double, std::string>> // (4) vecVariant2 = { 1.5, std::vector<int>{1, 2, 3, 4, 5}, "Hello "}; auto DisplayMe = Overload { // (5) [](std::vector<int>& myVec) { for (auto v: myVec) std::cout << v << " "; std::cout << '\n'; }, [](auto& arg) { std::cout << arg << '\n';}, }; for (auto v : vecVariant2) { // (6) std::visit(DisplayMe, v); } std::cout << '\n'; }
Line (1) creates a vector of variants having integral types, and line (4) creates a vector of variants having a std::vector<int>
, double
, and a std::string
.
Let me continue with the first variant vecVariant
. TypeOfIntegral (2) is an overload set that returns for a few integral types a string representation. If the type is not handled by the overload set, I return the string “unknown type
“. In line (3), I apply the overload set to each variant v
using std::visit
.
The second variant vecVariant2 (4), has composed types. I create an overload set (5) to display their values. In general, I can just push the value onto std:.cout
. For the std::vector<int>
, I use a range-based for-loop to push its values to std::cout
.
Finally, here is the output of the program.
I want to add a few words to the overload pattern used in this example (7). I already introduced in my last post, “Smart Tricks with Parameter Packs and Fold Expressions`.
template<typename ... Ts> // (1) struct Overload : Ts ... { using Ts::operator() ...; }; template<class... Ts> Overload(Ts...) -> Overload<Ts...>; // (2)
Line (1) is the overload pattern, and line (2) is the deduction guide. The struct Overload
can have arbitrarily many base classes (Ts ...
). It derives from each class public
and brings the call operator (Ts::operator..
.) of each base class into its scope. The base classes need an overloaded call operator (Ts::operator()). Lambdas provide this call operator. The following example is as simple as it can be.
#include <variant> template<typename ... Ts> struct Overload : Ts ... { using Ts::operator() ...; }; template<class... Ts> Overload(Ts...) -> Overload<Ts...>; int main(){ std::variant<char, int, float> var = 2017; auto TypeOfIntegral = Overload { // (1) [](char) { return "char"; }, [](int) { return "int"; }, [](auto) { return "unknown type"; }, }; }
Using this example in C++ Insights makes the magic transparent. First, call (1) causes the creation of a fully specialized class template.
Second, the used lambdas in the overload pattern such as [](char) { return "char"; }
cause the creation of a function object. In this case, the compiler gives the function object the name __lambda_15_9
.
Studying the auto-generate types show at least one interesting point. The call operator of __lambda_15_9 is overloaded for char: const char * operator() (char) const { return "char"; }
The deduction guide (template<class... Ts> Overload(Ts...) -> Overload<Ts...>;
) (line 2) is only needed for C++17. The deduction guide tells the compiler how to create out-of-constructor arguments template parameters. C++20 can automatically deduce the template.
What’s next?
The friendship of templates is special. In my next post, I will explain why.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
Hello,
In first example, as I expected, gcc doesn’t let std::variant be constructed because it’s a conflicting declaration.
error: conflicting declaration
std::variant v(“abc”) ;
note: previous declaration as ‘std::variant v’
std::variant v, w ;