Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does std::visit work with std::variant?

Tags:

I'm looking at std:variant/std::visit doc here: http://en.cppreference.com/w/cpp/utility/variant/visit and also googled a lot trying to understand the magic behind std::visit and std::variant.

So my question is the following. In the provided example, both in the polymorphic lambda and the "overloaded" there is some "magic" happening that makes it possible to extract the correct type from std::variant.

So looking at this:

for (auto& v: vec) {     std::visit(overloaded {         [](auto arg) { std::cout << arg << ' '; },         [](double arg) { std::cout << std::fixed << arg << ' '; },         [](const std::string& arg) { std::cout << std::quoted(arg) << ' '; },     }, v); } 

For each v, which is just a variant, how does the right overloaded lambda function being invoked? It seems there is some logic that needs to figure out the exact type held by the specific std::variant, cast it and dispatch it to the proper function. My question is how does it work? Same deal for this:

    std::visit([](auto&& arg) {         using T = std::decay_t<decltype(arg)>;         if constexpr (std::is_same_v<T, int>)             std::cout << "int with value " << arg << '\n';         else if constexpr (std::is_same_v<T, long>)             std::cout << "long with value " << arg << '\n';         else if constexpr (std::is_same_v<T, double>)             std::cout << "double with value " << arg << '\n';         else if constexpr (std::is_same_v<T, std::string>)             std::cout << "std::string with value " << std::quoted(arg) << '\n';         else              static_assert(always_false<T>::value, "non-exhaustive visitor!");     }, w); 

We pass polymorphic lambda to the visitor as the callable object and w is some variant that can hold int, long, double or std::string. Where is the logic that figures out the right type so using T = std::decay_t<decltype(arg)>; to retrieve the actual type of the specific instance of a variant?

like image 509
Kobi Avatar asked Dec 23 '17 21:12

Kobi


1 Answers

What I think, is that under the hood std::visit builds an array of function pointers (at compile time) which consists of instantiated function pointers for each type. The variant stores a run-time type index i (intgeral number) which makes it possible to select the right i-th function pointer and inject the value.

You might wonder how can we store function pointers with different argument types in a compile time array? -> This is done with type-erasure (I think), which means one stores functions with e.g. a void* argument, e.g. &A<T>::call:

template<typename T> struct A {    static call(void*p) { otherFunction(static_cast<T*>(p)); }  } 

where each call dispatches to the correct function otherFunction with the argument (this is your lambda at the end). Type erasure means the function auto f = &A<T>::call has no more notion of the type T and has signature void(*)(void*).

std::variant is really complicated and quite sophisticated as lots of powerful fancy metaprogramming tricks come into play. This answer might only cover the tip of the iceberg :-)

like image 82
Gabriel Avatar answered Sep 28 '22 07:09

Gabriel