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

a 夏天 提交于 2020-08-01 09:04:07

问题


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.


回答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 it's 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.




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



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



来源:https://stackoverflow.com/questions/52088928/trying-to-return-the-value-from-stdvariant-using-stdvisit-and-a-lambda-expre

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!