templates

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.

 templates

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. 

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

Be part of my mentoring programs:

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (starts March 2024)
  • Do you want to stay informed: Subscribe.

     

    // 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.

    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.

    visitVariants

    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.

    visitVariantsOverloadPattern

    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.

    OverloadPatternInstantiated

    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.

    lambdaChar

    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, Kris Kafka, 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, Dmitry Farberov, 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, moon, Philipp Lenk, Hobsbawm, and Charles-Jianye Chen.

    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

    Seminars

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

    Standard Seminars (English/German)

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

    • C++ – The Core Language
    • C++ – The Standard Library
    • C++ – Compact
    • C++11 and C++14
    • Concurrency with Modern C++
    • Design Pattern and Architectural Pattern with C++
    • Embedded Programming with Modern C++
    • Generic Programming (Templates) with C++
    • Clean Code with Modern C++
    • C++20

    Online Seminars (German)

    Contact Me

    Modernes C++ Mentoring,

     

     

    1 reply
    1. ThH
      ThH says:

      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 ;

      Reply

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *