10

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.

Justin
  • 24,288
  • 12
  • 92
  • 142
markf78
  • 597
  • 2
  • 7
  • 25
  • 2
    You are trying to achieve that type of k is run time determined to be one of variants. C++ is strongly typed language so you can't do it. – Öö Tiib Aug 30 '18 at 04:05
  • https://stackoverflow.com/questions/47956335/how-does-stdvisit-work-with-stdvariant – macroland Aug 30 '18 at 04:05
  • 2
    Types of expressions must be known at compile-time whereas the variant contained type is a runtime property; therefore you cannot do anythign like this. Instead you need to specify action to take for each possible contained type. – M.M Aug 30 '18 at 04:30
  • @ÖöTiib Actually the problem here arises as C++ is statically typed, not because of it's strong typing. Python is strong typed but dynamically typed, and so something similar has no problem going through over there. – Geezer Aug 30 '18 at 05:22
  • @SkepticalEmpiricist Yes strongly typed is loosely used term. I meant statically typed, that is strong in sense that type mismatches are caught compile time. – Öö Tiib Aug 30 '18 at 05:36
  • 3
    What are you planning to do with `k`? Can you show some code, even not working, but something that makes sense? – n. m. could be an AI Aug 30 '18 at 05:54
  • 2
    This question fundamentally makes no sense. You don't pull out the value, you visit it. – Barry Aug 30 '18 at 15:06

3 Answers3

11

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
);
bolov
  • 72,283
  • 15
  • 145
  • 224
10

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.

Geezer
  • 5,600
  • 18
  • 31
  • 1
    @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. – n. m. could be an AI Aug 30 '18 at 05:47
  • 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 Aug 30 '18 at 05:52
  • 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. – Öö Tiib Aug 30 '18 at 06:12
  • @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 – Geezer Apr 08 '22 at 19:43
2

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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524