Visiting a std::variant with the Overload Pattern

Contents[Show]

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.

 templates

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.

 

Rainer D 6 P2 540x540Modernes C++ Mentoring

Stay informed about my mentoring programs.

 

 

Subscribe via E-Mail.

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.

visitVariants

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.

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

OverloadPatternInstantiated

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.

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 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, Animus24, 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, Matthieu Bolt, 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, Dominik Vošček, and Rob North.

 

Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, and Slavko Radman.

 

 

My special thanks to Embarcadero CBUIDER STUDIO FINAL ICONS 1024 Small

 

My special thanks to PVS-Studio PVC Logo

 

My special thanks to Tipi.build tipi.build logo

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.

  • 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++

New

  • Clean Code with Modern C++
  • C++20

Contact Me

Modernes C++,

RainerGrimmDunkelBlauSmall

 
 

 

 

Comments   

0 #1 Chris 2022-06-21 13:35
Thanks for the overload pattern. One issue with the pattern as shown is that if you want to use a templated lambda, the compiler (g++) will complain.
Quote

Mentoring

Stay Informed about my Mentoring

 

English Books

Course: Modern C++ Concurrency in Practice

Course: C++ Standard Library including C++14 & C++17

Course: Embedded Programming with Modern C++

Course: Generic Programming (Templates)

Course: C++ Fundamentals for Professionals

Interactive Course: The All-in-One Guide to C++20

Subscribe to the newsletter (+ pdf bundle)

All tags

Blog archive

Source Code

Visitors

Today 280

Yesterday 8050

Week 8330

Month 198406

All 11679560

Currently are 160 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments