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?
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 :-)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With