Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trying to return the value from std::variant using std::visit and a lambda expression

Tags:

c++

c++17

Suppose there exists a variant v defined as follows:

std::variant<int,char,double,bool,std::string> v;

I am trying to get the underlying value from a std::variant using std::visit or std::get.

I tried doing this naively:

constexpr size_t idx = v.index();
auto k = std::get<idx>(v);

But then learned that this will fail if the variant v is not a constexpr itself. And even then there may be problems with using std::string (due to the definition of the destructor for std::string).

My 2nd attempt was to try and do the following:

auto k = std::visit([](auto arg){return arg;}, v);

But received this:

$g++ -o main *.cpp --std=c++17
In file included from main.cpp:5:0:
/usr/include/c++/7/variant: In instantiation of ‘static constexpr auto std::__detail::__variant::__gen_vtable_impl<std::__detail::__variant::_Multi_array<_Result_type (*)(_Visitor, _Variants ...)>, std::tuple<_Tail ...>, std::integer_sequence<long unsigned int, __indices ...> >::_S_apply() [with _Result_type = int; _Visitor = main()::<lambda(auto:1)>&&; _Variants = {std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&}; long unsigned int ...__indices = {1}]’:
/usr/include/c++/7/variant:663:61:   required from ‘static constexpr void std::__detail::__variant::__gen_vtable_impl<std::__detail::__variant::_Multi_array<_Result_type (*)(_Visitor, _Variants ...), __dimensions ...>, std::tuple<_Variants ...>, std::integer_sequence<long unsigned int, __indices ...> >::_S_apply_single_alt(_Tp&) [with long unsigned int __index = 1; _Tp = std::__detail::__variant::_Multi_array<int (*)(main()::<lambda(auto:1)>&&, std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&)>; _Result_type = int; _Visitor = main()::<lambda(auto:1)>&&; long unsigned int ...__dimensions = {5}; _Variants = {std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&}; long unsigned int ...__indices = {}]’
/usr/include/c++/7/variant:651:39:   required from ‘constexpr const std::__detail::__variant::_Multi_array<int (*)(main()::<lambda(auto:1)>&&, std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&), 5> std::__detail::__variant::__gen_vtable<int, main()::<lambda(auto:1)>&&, std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&>::_S_vtable’
/usr/include/c++/7/variant:704:29:   required from ‘struct std::__detail::__variant::__gen_vtable<int, main()::<lambda(auto:1)>&&, std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&>’
/usr/include/c++/7/variant:1239:23:   required from ‘constexpr decltype(auto) std::visit(_Visitor&&, _Variants&& ...) [with _Visitor = main()::<lambda(auto:1)>; _Variants = {std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&}]’
main.cpp:89:49:   required from here
/usr/include/c++/7/variant:704:49:   in constexpr expansion of ‘std::__detail::__variant::__gen_vtable<int, main()::<lambda(auto:1)>&&, std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&>::_S_apply()’
/usr/include/c++/7/variant:701:38:   in constexpr expansion of ‘std::__detail::__variant::__gen_vtable_impl<std::__detail::__variant::_Multi_array<int (*)(main()::<lambda(auto:1)>&&, std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&), 5>, std::tuple<std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&>, std::integer_sequence<long unsigned int> >::_S_apply()’
/usr/include/c++/7/variant:641:19:   in constexpr expansion of ‘std::__detail::__variant::__gen_vtable_impl<std::__detail::__variant::_Multi_array<int (*)(main()::<lambda(auto:1)>&&, std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&), 5>, std::tuple<std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&>, std::integer_sequence<long unsigned int> >::_S_apply_all_alts<0, 1, 2, 3, 4>(\xe2\x80\x98result_dec\xe2\x80\x99 not supported by dump_expr#<expression error>, (std::make_index_sequence<5>(), std::make_index_sequence<5>()))’
/usr/include/c++/7/variant:686:43: error: invalid conversion from ‘std::__success_type<char>::type (*)(main()::<lambda(auto:1)>&&, std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&) {aka char (*)(main()::<lambda(auto:1)>&&, std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&)}’ to ‘int (*)(main()::<lambda(auto:1)>&&, std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&)’ [-fpermissive]
       { return _Array_type{&__visit_invoke}; }
                                           ^

I'm stuck as to why the std::visit call does not work. I thought I supplied a trivial lambda expression which takes all possible types for the variant and returns the underlying value but it appears I am misunderstanding something.

I want to use std::variant now (after initially considering std::any (see avoid writing the same repetitive type-checking code with std::any) but I need a way to return the contained value. Any help would be greatly appreciated. Thank you very much.

like image 423
markf78 Avatar asked Aug 30 '18 03:08

markf78


People also ask

What does std :: visit do?

std::visitApplies the visitor vis (Callable that can be called with any combination of types from variants) to the variants vars .

What is STD Monostate?

std::monostate is a class that has exactly one value. It is default constructable and supports all the comparison operations. std::monostate is about as simple of a type as one could concoct. These properties turn out to be useful for writing template code. The first use case is in testing.

What is boost :: variant?

Boost. Variant, part of collection of the Boost C++ Libraries. It is a safe, generic, stack-based discriminated union container, offering a simple solution for manipulating an object from a heterogeneous set of types in a uniform manner.


3 Answers

I am trying to get the underlying value from a std::variant using std::visit or std::get.

If what you want is indeed holding the underlying current value, then you must have the visitation support a specific handling of each one possible. For instance, like this:

std::visit([] (const auto& var) {
    if constexpr (std::is_same_v<std::decay_t<decltype(var)>, int>) {
        // Do something with var of type int
    }
    else if constexpr (std::is_same_v<std::decay_t<decltype(var)>, char>) {
        // Do something with var of type char
    }
    else if constexpr (std::is_same_v<std::decay_t<decltype(var)>, double>) {
        // Do something with var of type double
    }
    else if constexpr (std::is_same_v<std::decay_t<decltype(var)>, bool>) {
        // Do something with var of type bool
    }
    else if constexpr (std::is_same_v<std::decay_t<decltype(var)>, std::string>) {
        // Do something with var of type std::string
    }
}, v);

This is becasue C++ is a static typed language, as in, all types of variables must be known in compile time. Thus, the compiler cannot allow you to just declare auto and be done with it when you want what could be one of various types that the std::variant may hold as the current value at that moment during run-time.

... but I need a way to return the contained value.

Being statically typed, there's no way in C++ to do so without going through the possible cases. If you want a call that takes an instance of such std::variant and returns, say, a std::string, then you can modify the above code to return std::to_string(var) for each case in the above if/else statement.

  • Note this makes use of the constexpr if keyword. Worth reading into in case it is unclear why this is needed here. The use of std::decay_t template is needed to make sure the type compared in the std::is_same_v template is of the basic (non-const and non-reference qualified) type.

Taken from the comments:

Then why does std::visit([](auto&& arg){std::cout << arg;}, v); work?

This works because you're not trying to assign/copy the variable of the underlying type into a variable of your own. This, again, would have required knowing the type for such a variable during compilation. But when std::variant is being required to provide a string representation of its currently held value -- for example due to operator << of std::cout -- then internally what it does is of the same semantics as our if-else switch above, i.e. handling differently for each possible underlying type of this variant instance.

Clarification: There is obviously more than one way to specify handling of the different possibilities of what the std::variant instance might currently be holding. For example, as shown in the std::visit cppreference page, you could be using the template deduction guides based std::visit(overloaded { ... way of doing it, that while arguably makes for better and shorter code, it takes some deeper explaining to understand the mechanics of, the way I see it, as it includes inheriting from a lambda, among other things, and so I figured it to be beyond the explanatory scope of this answer in regards to how I understand the question being asked. You can read all about it here and here. Or easier see the usage code example in another answer to this question.


Regarding the compilation errors: This will compile for you just fine, but it doesn't achieve what you wanted:

using your_variant = std::variant<int,char,double,bool,std::string>;
your_variant v;

auto k = std::visit([](auto arg)-> your_variant {return arg;}, v);

Your lines didn't compile as the lambda needs to declare it's return type by -> your_variant explicitly as the compiler has no way of inferring it from the lambda.

Another valid syntax to solve the same problem is just declaring the parameter type, so the compiler can know what it's returning as if it was a function returning auto:

auto k2 = std::visit([](your_variant arg) {return arg;}, v);

The compilation problem with doing this:

constexpr size_t idx = v.index();
auto k = std::get<idx>(v);

is again, due to static typing, that v could hold any single one of its indices at run-time, and the template argument for std::get() needs to be known at compile time.

like image 116
Geezer Avatar answered Oct 06 '22 00:10

Geezer


What you are trying to do can't work because the type of the object variant holds is known at runtime and the type of the variable where you want to store it must be known at compile time.

The pattern to deal with this variant is to do the work on a template function that can deal with any type, or to have a set of overloads that can accept any type from the variant.

Option 1

Do all work on a template function:

std::visit([] (const auto& k) { std::cout << k; }, v);

Or, inside the function differentiate with constexpr if. But I don't see the point of this one as there is a better alternative imo with overloads (see next):

std::visit([] (const auto& k) {
        using T = std::decay_t<decltype(k)>;
        if constexpr (std::is_same_v<T, int>)
            std::cout << "int with value " << k << '\n';
        else if constexpr (std::is_same_v<T, char>)
            std::cout << "char with value " << k << '\n';
        else if constexpr (std::is_same_v<T, double>)
            std::cout << "double with value " << k << '\n';
        else if constexpr (std::is_same_v<T, std::string>)
            std::cout << "std::string with value " << k << '\n';
    }, v);

Option 2

Call different overloads

template <class... Fs> struct Overload : Fs... { using Fs::operator()...; };
template <class... Fs> Overload(Fs...) -> Overload<Fs...>;
std::visit(
    Overload{
        [] (int k) { /* deal with k here */ },
        [] (char k) { /* deal with k here */ },
        [] (double k) { /* deal with k here */ },
        [] (bool k) { /* deal with k here */ },
        [] (std::string k) { /* deal with k here */ }
    },
    v
);
like image 40
bolov Avatar answered Oct 05 '22 23:10

bolov


Every variable in a given C++ function has a single, fixed type.

auto k = std::visit([](auto arg){return arg;}, v);

here you want k to have one of multiple different types. C++ does not support this.

"But", you say, "why does":

std::visit([](auto arg){std::cout << arg;}, v);

work? In the lambda, arg takes many different types!

That is because [](auto arg){...} is not a single function, but (shorthand) for a template function. A template function is not a function, but a template for creating functions.

That code causes N different functions to be created, each with a different type for auto arg. They are all compiled. Then, std::visit picks one to run.

std::variant is how we store multiple different possible types of data in one variable. It has a fixed type, but it exposes visit so you can type-safely get at the underlying data.

Now things aren't that bad. You can just put your code in the lambda.

So instead of:

auto k = std::visit([](auto arg){return arg;}, v);
// code using k

do this:

std::visit([](auto k){
  // code using k
}, v);

If you want to return a value, you have to go back into the land of std::variant. Suppose you want to return std::vector<T> where T is the type in the variant.

template<class...Ts>
using var_of_vec = std::variant< std::vector<Ts>... >;
using my_vector = var_of_vec<int,char,double,bool,std::string>;

my_vector v =std::visit([](auto k)->my_vector{
  std::vector<decltype(k)> vec;
  // code using k
  return vec;
}, v);

within the body of the lambda you use a single vector, then you return a variant of vectors.

like image 20
Yakk - Adam Nevraumont Avatar answered Oct 05 '22 23:10

Yakk - Adam Nevraumont