为什么 std::visit 必须有一个 return 类型?

Why must std::visit have a single return type?

在研究 std::variantstd::visit 时,出现了以下问题:

考虑以下代码:

using Variant = std::variant<int, float, double>;

auto lambda = [](auto&& variant) {
  std::visit(
    [](auto&& arg) {
      using T = std::decay_t<decltype(arg)>;
      if constexpr (std::is_same_v<T, int>) {
        std::cout << "int\n";
      } else if (std::is_same_v<T, float>) {
        std::cout << "float\n";
      } else {
        std::cout << "double\n";
      }
    },
  variant);
};

它工作正常,如下例所示:

lambda(Variant(4.5));    // double
lambda(Variant(4.f));    // float
lambda(Variant(4));      // int

那为什么下面的会失败:

using Variant = std::variant<int, float, double>;

auto lambda = [](auto&& variant) {
  std::visit([](auto&& arg) { return arg; }, variant);
};

auto t = lambda(Variant(4.5));

由于静态断言

static_assert failed due to requirement '__all<is_same_v<int
      (*)(__value_visitor<(lambda at main.cc:25:7)> &&,
      __base<std::__1::__variant_detail::_Trait::_TriviallyAvailable, int, float,
      double> &), float (*)(__value_visitor<(lambda at main.cc:25:7)> &&,
      __base<std::__1::__variant_detail::_Trait::_TriviallyAvailable, int, float,
      double> &)>, is_same_v<int (*)(__value_visitor<(lambda at main.cc:25:7)> &&,
      __base<std::__1::__variant_detail::_Trait::_TriviallyAvailable, int, float,
      double> &), double (*)(__value_visitor<(lambda at main.cc:25:7)> &&,
      __base<std::__1::__variant_detail::_Trait::_TriviallyAvailable, int, float,
      double> &)> >::value' "`std::visit` requires the visitor to have a single
      return type."

std::visit 显然可以推导出 arg 的类型,如成功示例所示。那为什么要求只有一个 return 类型呢?

编译器 Apple LLVM version 10.0.1 (clang-1001.0.46.4)gcc version 8.3.0 失败并出现类似错误。

尽管每个 "implementation" 都是不同的重载,因此可能具有不同的 return 类型,但在某些时候您将需要一个公共访问点,而该公共访问点将需要一个单一 return 类型,因为所选变体类型仅在运行时已知。

访问者在 visit 代码中执行该逻辑是常见的约定;事实上,std::visit 的真正目的就是为您施展魔法并抽象出运行时类型切换。

否则,您基本上会在调用站点重新实现 std::visit

很容易认为这一切都可以使用模板来解决:毕竟,您已经使用了通用的 lambda,所以所有这些重载都是自动实例化的,那么为什么 return 类型不能只是 "known"?同样,它只在运行时才知道,所以这对你没有好处。必须有某种静态方式将访问结果传送到调用站点。

std::visit 的 return 类型仅取决于访问者的类型和传递给它的变体。这就是 C++ 类型系统的工作原理。

如果您想要 std::visit 到 return 一个值,该值需要已经有一个 compile-time 的类型,因为在 C++ 中所有变量和表达式都是静态类型。

您在该特定行中传递 Variant(4.5)(因此“显然访问将 return 是双精度”)这一事实不允许编译器改变类型系统的规则- std::visit return type 根本无法根据您传递的变体 value 进行更改,并且无法决定仅在访问者的 type 和变体的 type 中的一个 return 类型上。其他一切都会产生极其奇怪的后果。

This 维基百科文章实际上基本上讨论了您所拥有的确切 situation/question,只是使用 if 而不是更详尽的 std::visit 版本:

For example, consider a program containing the code:

if <complex test> then <do something> else <signal that there is a type error>

Even if the expression always evaluates to true at run-time, most type checkers will reject the program as ill-typed, because it is difficult (if not impossible) for a static analyzer to determine that the else branch will not be taken.


如果您希望 returned 类型为“variant-ish”,您必须坚持使用 std::variant。例如,您仍然可以这样做:

auto rotateTypes = [](auto&& variant) {
  return std::visit(
    [](auto&& arg) -> std::variant<int, float, double> {
      using T = std::decay_t<decltype(arg)>;
      if constexpr (std::is_same_v<T, int>) {
        return float(arg);
      } else if (std::is_same_v<T, float>) {
        return double(arg);
      } else {
        return int(arg);
      }
    },
  variant);
};

std::visit 推导出的 return 类型是 std::variant<int, float, double> - 只要你不决定一种类型,你必须留在一个变体中(或在单独的模板中)实例化)。您不能“欺骗”C++ 放弃在变体上使用 identity-visitor 的静态类型。