Visiting std::variant with the Overload Pattern

Visiting std::variant with the Overload Pattern

This post is a cross-post from www.ModernesCpp.com .

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 very handy into play.

I introduced in my last post "Smart Tricks with Parameter Packs and Fold Expressions ` 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 line (2) - (3) you see three possibilities to assign the 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 by 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 call or assignment call is unambiguous, a simple conversion takes place. 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 posts "Everything You Need to Know About std::variant from C++17 " by Bartlomiej Filipek.

Thanks to the function std::visit, C++17 provides a convenient way to visits the elements of a std::variant.

std::visit

What sounds like the visitor pattern according to the classical design patterns is really a kind of a visitor for a container of variants.

std::visit allows you to apply a visitor to a container of variants. The visitor must be a callable. A callable is something, which 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 quite 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.

No alt text provided for this image

Fine? No!. I used in the program visitVariant.cpp a 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 variants. 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) 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.

No alt text provided for this image

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 for it. The struct Overload can have arbitrary 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 make the magic transparent. First, call (1) causes the creation of a fully specialized class template.

No alt text provided for this image

Second, the used lambdas in the overload pattern such as [](char) { return "char"; } causes the creation of a function object. In this case, the compiler gives the function object the name __lambda_15_9.

No alt text provided for this image

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 explain why.

?

Thanks a lot to my Patreon Supporters : Matt Braun, Roman Postanciuc, Tobias Zindl, Marko, G Prvulovic, Reinhold Dr?ge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Louis St-Amour, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Neil Wang, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Tobi Heideman, Daniel Hufschl?ger, Red Trip, Alexander Schwarz, Tornike Porchxidze, Alessandro Pezzato, Evangelos Denaxas, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Dimitrov Tsvetomir, Leo Goodstadt, Eduardo Velasquez, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, and Robin Furness.

Thanks in particular to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, and Said Mert Turkal.

My special thanks to Embarcadero

?

Seminars

I'm happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

Bookable (Online)

German

Standard Seminars (English/German)

Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

New

Contact Me

Modernes C++


要查看或添加评论,请登录

社区洞察

其他会员也浏览了