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

11,666

Solution 1

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:

#include <string>
#include <variant>

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

    std::visit([](your_variant&& arg) {
        if (std::holds_alternative<int>(arg))
            auto v_int = std::get<int>(arg);
        else if (std::holds_alternative<char>(arg))
            auto v_chart = std::get<char>(arg);
        else if (std::holds_alternative<double>(arg))
            auto v_double = std::get<double>(arg);
        else if (std::holds_alternative<bool>(arg))
            auto v_bool = std::get<bool>(arg);
        else if (std::holds_alternative<std::string>(arg))
            auto v_str = std::get<std::string>(arg);
        }, v);

    return 0;
}

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 function 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() for each value returned from std::get() (redundant but just for illustration). Then you'll have the contained type in the "non-std::variant form".

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.

Solution 2

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
);

Solution 3

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.

Share:
11,666
markf78
Author by

markf78

I write code for embedded controllers in C/C++/C# that are used in manufacturing.

Updated on June 05, 2022

Comments

  • markf78
    markf78 almost 2 years

    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.

  • n. m.
    n. m. over 5 years
    @markf78 "why does std::visit([](auto&& arg){std::cout << arg;}, v); work?" Because that auto in the lambda argument list denotes a template. There is a switch or an equivalent inside std::visit, and each branch calls a different instantiation of a template.
  • Öö Tiib
    Öö Tiib over 5 years
    The first example is irrelevant to OP since it does not return a value. May be change it to something that converts every option to value of one type for example std::string.
  • Öö Tiib
    Öö Tiib over 5 years
    I meant that if OP wants to get a value using visit from variant then it has to be of certain type like string otherwise it is still variant, union or any and so there are still if else chains on type.
  • markf78
    markf78 over 5 years
    The using statement does not compile.
  • Geezer
    Geezer about 2 years
    @Holger I can however agree the opening illustrative example might unintentionally be passing a bad practice as an advice -- due to the redundant extraction->rewrap misuse you have pointed out. I have fixed it now, hoping you'll agree with the current form. Reason I avoided writing it like this in the first place was the necessary use of constexpr if which requires understanding in its own right -- and what was important to me was, again, getting the point across wrt static typing. I now feel like a better choice is not avoiding it but making a note of it like I did in the edit. Thank you