Alex
Alex

Reputation: 3566

Why can I not retrieve the index of a variant and use that to get its content?

I'm trying to access the content of a variant. I don't know what's in there, but thankfully, the variant does. So I thought I'll just ask the variant what index it is on and then use that index to std::get its content.

But this does not compile:

#include <variant>

int main()
{
  std::variant<int, float, char> var { 42.0F };

  const std::size_t idx = var.index();

  auto res = std::get<idx>(var);

  return 0;
}

The error happens in the std::get call:

error: no matching function for call to ‘get<idx>(std::variant<int, float, char>&)’
   auto res = std::get<idx>(var);
                               ^
In file included from /usr/include/c++/8/variant:37,
                 from main.cpp:1:
/usr/include/c++/8/utility:216:5: note: candidate: ‘template<long unsigned int _Int, class _Tp1, class _Tp2> constexpr typename std::tuple_element<_Int, std::pair<_Tp1, _Tp2> >::type& std::get(std::pair<_Tp1, _Tp2>&)’
     get(std::pair<_Tp1, _Tp2>& __in) noexcept
     ^~~
/usr/include/c++/8/utility:216:5: note:   template argument deduction/substitution failed:
main.cpp:9:31: error: the value of ‘idx’ is not usable in a constant expression
   auto res = std::get<idx>(var);
                               ^
main.cpp:7:15: note: ‘std::size_t idx’ is not const
   std::size_t idx = var.index();
               ^~~

How can I fix this?

Upvotes: 13

Views: 3236

Answers (5)

einpoklum
einpoklum

Reputation: 131523

Essentially, you cannot.

You wrote:

I don't know what's in there, but thankfully, the variant does

... but only at run-time, not at compile-time.
And that means your idx value is not compile-time.
And that means you can't use get<idx>() directly.

Something you could do is have a switch statement; ugly, but it would work:

switch(idx) {
case 0: { /* code which knows at compile time that idx is 0 */ } break;
case 1: { /* code which knows at compile time that idx is 1 */ } break;
// etc. etc.
}

This is rather ugly however. As comments suggest, you might as well std::visit() (which is not very different from the code above, except using variadic template arguments instead of being this explicit) and avoid the switch altogether. For other index-based approaches (not specific to std::variant), see:

Idiom for simulating run-time numeric template parameters?

Upvotes: 4

elbrunovsky
elbrunovsky

Reputation: 440

The compiler needs to know the value of idx at compilation time for std::get<idx>() to work, because it is being used as a template argument.

First option: If the code is meant to run at compile time, then make everything constexpr:

constexpr std::variant<int, float, char> var { 42.0f };

constexpr std::size_t idx = var.index();

constexpr auto res = std::get<idx>(var);

This works because std::variant is constexpr friendly (its constructors and methods are all constexpr).

Second option: If the code is not meant to run at compile time, which is likely the case, the compiler cannot deduce at compile time the type of res, because it could be three different things (int, float or char). C++ is a statically-typed language, and the compiler must be able to deduce the type of auto res = ... from the expression that follows (i.e. it must always be the same type).

You can use std::get<T>, with the type instead of an index, if you already know what it will be:

std::variant<int, float, char> var { 42.0f }; // chooses float

auto res = std::get<float>(var);

In general, use std::holds_alternative to check if the variant is holding each of the given types, and handle them separately:

std::variant<int, float, char> var { 42.0f };

if (std::holds_alternative<int>(var)) {
    auto int_res = std::get<int>(var); // int&
    // ...
} else if (std::holds_alternative<float>(var)) {
    auto float_res = std::get<float>(var); // float&
    // ...
} else {
    auto char_res = std::get<char>(var); // char&
    // ...
}

Alternatively you can use std::visit. This is slightly more complicated: you can use a lambda/templated function that is type-agnostic and works for all of the variant's types, or pass a functor with an overloaded call operator:

std::variant<int, float, char> var { 42.0f };

std::size_t idx = var.index();

std::visit([](auto&& val) {
    // use val, which may be int&, float& or char&
}, var);

See std::visit for details and examples.

Upvotes: 8

Davis Herring
Davis Herring

Reputation: 39818

This is inherently impossible in C++’s model; consider

template<class T> void f(T);
void g(std::variant<int,double> v) {
  auto x=std::get<v.index()>(v);
  f(x);
}

Which f is being called, f<int> or f<double>? If it’s “both”, that means that g contains a branch (which it doesn’t), or that there are two versions of g (which just pushes the problem onto its caller). And think about f(T,U,V,W)—where does the compiler stop?

There is actually a proposal for a JIT for C++ that would allow things like this by compiling those additional versions of f when they’re called, but it’s very early.

Upvotes: 1

patatahooligan
patatahooligan

Reputation: 3321

The problem arises from templates being instantiated at compile-time while the index you are getting is calculated at run-time. Similarly, C++ types are also defined at compile-time so even with the auto declaration, res must have a concrete type for the program to be well-formed. This means that even without the restriction on the template, what you are trying to do is inherently impossible for non-constant expression std::variants. How would one work around this?

Firstly, if your variant is indeed a constant expression, the code compiles and works as expected

#include <variant>

int main()
{
  constexpr std::variant<int, float, char> var { 42.0f };

  constexpr std::size_t idx = var.index();

  auto res = std::get<idx>(var);

  return 0;
}

Otherwise you will have to use some manual branching mechanism

if (idx == 0) {
    // Now 'auto' will have a concrete type which I've explicitly used
    int value == std::get<0>(var);
}

You could define these branches using the visitor pattern, see std::visit.

Upvotes: 2

max66
max66

Reputation: 66200

The problem is that std::get<idx>(var); require (for idx) a compile time known value.

So a constexpr value

// VVVVVVVVV
   constexpr std::size_t idx = var.index();

But to initialize idx as constexpr, also var had to be constexpr

// VVVVVVVVV
   constexpr std::variant<int, float, char> var { 42.0F };

Upvotes: 3

Related Questions