Dlaczego nie mogę pobrać indeksu wariantu i użyć go, aby uzyskać jego zawartość?

10

Próbuję uzyskać dostęp do zawartości wariantu. Nie wiem co tam jest, ale na szczęście wariant wie. Pomyślałem więc, że po prostu zapytam wariant, na jakim jest indeksie, a następnie użyję tego indeksu do std::getjego zawartości.

Ale to się nie kompiluje:

#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;
}

Błąd występuje w std::getwywołaniu:

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();
               ^~~

Jak mogę to naprawić?

Alex
źródło
3
Podejrzewam, że występujący błąd jest związany z tym, że indeks nie jest stałym wyrażeniem. Opublikuj komunikaty o błędach kompilatora, abyśmy mogli udzielić znaczącej pomocy.
patatahooligan
Brakuje constexpr?
Rlyeh
Ups! Mówiłeś o błędzie, ale nie opublikowałeś dokładnego tekstu błędu.
Jonathan Wood
1
Przepraszam za pominięcie, zaktualizowałem pytanie
Alex

Odpowiedzi:

4

Zasadniczo nie możesz.

Napisałeś:

Nie wiem co tam jest, ale na szczęście wariant wie

... ale tylko w czasie wykonywania, a nie w czasie kompilacji.
A to oznacza, że ​​twoja idxwartość nie jest czasem kompilacji.
A to oznacza, że ​​nie możesz używać get<idx>()bezpośrednio.

Coś, co możesz zrobić, to mieć instrukcję switch; brzydkie, ale zadziałałoby:

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.
}

To jest raczej brzydkie. Jak sugerują komentarze, możesz również std::visit()(co nie różni się bardzo od powyższego kodu, z wyjątkiem używania argumentów szablonów variadic zamiast jawnego) i całkowicie uniknąć przełączania. W przypadku innych podejść opartych na indeksach (nie specyficznych dla std::variant) zobacz:

Idiom do symulowania parametrów szablonów numerycznych w czasie wykonywania?

einpoklum
źródło
@Caleth: Tak. Edytowane.
einpoklum
5

Aby kompilator mógł działać, musi znać wartość idxczasu kompilacji std::get<idx>(), ponieważ jest używany jako argument szablonu.

Pierwsza opcja: jeśli kod ma działać w czasie kompilacji, wykonaj następujące czynności constexpr:

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

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

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

Działa std::variantto, ponieważ jest constexprprzyjazne (wszystkie jego konstruktory i metody constexpr).

Druga opcja: Jeżeli kod nie jest przeznaczona do pracy w czasie kompilacji, co jest prawdopodobne w przypadku, kompilator nie można wywnioskować, w czasie kompilacji typu res, ponieważ może to być trzy różne rzeczy ( int, floatlub char). C ++ jest językiem o typie statycznym, a kompilator musi być w stanie wydedukować typ auto res = ...z następującego po nim wyrażenia (tzn. Zawsze musi być tego samego typu).

Możesz użyć std::get<T>z typem zamiast indeksu, jeśli już wiesz, co to będzie:

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

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

Zasadniczo należy użyć, std::holds_alternativeaby sprawdzić, czy wariant zawiera każdy z podanych typów, i obsługiwać je osobno:

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&
    // ...
}

Alternatywnie możesz użyć std::visit. Jest to nieco bardziej skomplikowane: możesz użyć funkcji lambda / templated, która jest niezależna od typu i działa dla wszystkich typów wariantów, lub przekazać funktor z przeciążonym operatorem wywołania:

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

Zobacz szczegóły std :: visit i przykłady.

Elbrunowski
źródło
3

Problem polega na tym, że std::get<idx>(var);wymaga (dla idx) znanej wartości czasu kompilacji.

Więc constexprwartość

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

Ale aby zainicjować idxtak constexpr, jak varmusiało byćconstexpr

// VVVVVVVVV
   constexpr std::variant<int, float, char> var { 42.0F };
max66
źródło
… A wariant constexpr nie jest bardzo wariantem.
Davis Herring
@DavisHerring - to także prawda.
max66
2

Problem wynika z tworzenia instancji szablonów w czasie kompilacji, podczas gdy otrzymywany indeks jest obliczany w czasie wykonywania. Podobnie typy C ++ są również definiowane w czasie kompilacji, więc nawet w przypadku autodeklaracji resmusi mieć konkretny typ, aby program był poprawnie sformułowany. Oznacza to, że nawet bez ograniczenia w szablonie, to, co próbujesz zrobić, jest z natury niemożliwe dla niestałych wyrażeń std::variant. Jak można to obejść?

Po pierwsze, jeśli twój wariant jest rzeczywiście stałym wyrażeniem, kod kompiluje się i działa zgodnie z oczekiwaniami

#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;
}

W przeciwnym razie będziesz musiał użyć ręcznego mechanizmu rozgałęziania

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

Możesz zdefiniować te gałęzie za pomocą wzorca odwiedzającego, patrz std :: visit .

patatahooligan
źródło
1

Jest to z natury niemożliwe w modelu C ++; rozważać

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

Który fjest nazywany, f<int>lub f<double>? Jeśli jest to „oba”, oznacza to, że gzawiera gałąź (której nie ma) lub że istnieją dwie wersje g(co po prostu popycha problem na jego wywołującego). Zastanów się f(T,U,V,W)- gdzie kończy się kompilator?

W rzeczywistości istnieje propozycja JIT dla C ++, która pozwoliłaby na takie rzeczy, kompilując te dodatkowe wersje, fkiedy są wywoływane, ale jest to bardzo wcześnie.

Davis Herring
źródło