为什么我不能检索变体的索引并使用它来获取其内容?

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

我正在尝试访问变体的内容。我不知道里面有什么,但谢天谢地,变种确实知道。所以我想我会问变体它在什么索引上,然后使用该索引 std::get 它的内容。

但这不能编译:

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

错误发生在 std::get 调用中:

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

我该如何解决这个问题?

问题是 std::get<idx>(var); 需要(对于 idx)一个编译时已知值。

所以 constexpr

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

但是要将 idx 初始化为 constexprvar 也必须是 constexpr

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

问题出在编译时实例化模板,而您获得的索引是在 运行 时计算的。类似地,C++ 类型也在编译时定义,因此即使使用 auto 声明,res 也必须具有具体类型才能使程序格式良好。这意味着即使没有对模板的限制,对于非常量表达式 std::variant 来说,你试图做的事情本质上是不可能的。如何解决这个问题?

首先,如果你的变体确实是一个常量表达式,代码会按预期编译和工作

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

否则你将不得不使用一些手动分支机制

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

您可以使用访问者模式定义这些分支,参见 std::visit

基本上,你不能。

您写道:

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

...但仅在 运行 时,而不是在编译时。
这意味着您的 idx 值不是编译时。
意味着你不能直接使用get<idx>()

你可以做的是有一个 switch 语句;丑陋,但它会起作用:

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

然而,这相当丑陋。正如评论所建议的那样,您也可以 std::visit() (这与上面的代码没有太大区别,除了使用可变参数模板参数而不是如此显式)并完全避免切换。对于其他基于索引的方法(不特定于 std::variant),请参阅:

编译器需要在编译时知道 idx 的值,std::get<idx>() 才能工作,因为它被用作模板参数。

第一个选项:如果代码要在编译时 运行,则将所有内容设为 constexpr:

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

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

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

这是可行的,因为 std::variantconstexpr 友好的(它的构造函数和方法都是 constexpr)。

第二个选项:如果代码在编译时不打算 运行(很可能是这种情况),编译器无法在编译时推断出 res 的类型,因为它可能是三个不同的东西(intfloatchar)。 C++ 是一种静态类型语言,编译器必须能够从后面的表达式中推断出 auto res = ... 的类型(即它必须始终是同一类型)。

您可以使用 std::get<T> 和类型而不是索引,如果您已经知道它将是什么:

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

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

一般来说,使用std::holds_alternative检查变体是否持有每个给定的类型,并分别处理它们:

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

或者您可以使用 std::visit。这稍微复杂一些:您可以使用与类型无关并适用于所有变体类型的 lambda/templated 函数,或者传递带有重载调用运算符的仿函数:

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

有关详细信息和示例,请参阅 std::visit

这在C++的模型中是天生不可能的;考虑

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

正在调用哪个 ff<int>f<double>?如果它是“两者”,则意味着 g 包含一个分支(它没有),或者 g 有两个版本(这只是将问题推给了它的调用者)。想想 f(T,U,V,W)——编译器在哪里停止?

实际上有一个用于 C++ 的 JIT 的 proposal,它可以通过在调用 f 的那些附加版本时编译它们来允许这样的事情,但它还很早。