通过使用std::visit,从std::variant的可能类型中返回值

3

我正在努力理解std::variantstd::visit,并尝试想出一种方法来指定我希望我的变量存储的几种类型(这些类型将进入我的std::variant),然后通过std::visit检索存储的数据。考虑以下示例:

#include <iostream>
#include <variant>
#include <string>

struct PrintType {
  void operator()(const int &data) {
    std::cout << "visiting int node" << std::endl;
  }
  void operator()(const double &data) {
    std::cout << "visiting double node" << std::endl;
  }
};

struct SingleOperatorOverload {
  int operator()(const int &data) {
    std::cout << "visiting int node" << std::endl;
    return data;
  }
};

struct AllTypesOperatorOverload {
  int operator()(const int &data) {
    std::cout << "visiting int node" << std::endl;
    return data;
  }
  double operator()(const double &data) {
    std::cout << "visiting double node" << std::endl;
    return data;
  }
};

int main() {

  using var_t = std::variant<int, double>;

  // print int related operator() content, OK
  var_t foo = 42;
  std::visit(PrintType(), foo);

  // print double related operator() content, OK
  foo = 3.1415;
  std::visit(PrintType(), foo);

  // get value and store into bar, struct with single operator(), OK
  foo = 42;
  auto bar = std::visit(SingleOperatorOverload(), foo);
  std::cout << "bar: " << bar << std::endl;

  // get value and store into bar, struct with multiple operator(), ERROR
  auto bar = std::visit(AllTypesOperatorOverload(), foo);
  std::cout << "bar: " << bar << std::endl;

  return 0;
}

这个变量(在这个简化的例子中)可以保存intdouble类型。如果我只想基于类型打印一些东西(就像PrintType结构体中所做的那样),那么它可以正常工作。
如果我想通过访问者来检索数据(就像SingleOperatorOverload类中所做的那样),该类仅为接受int类型参数的operator()提供实现,那么这也可以正常工作。然而,一旦我尝试为std::variant中的每种类型,即intdouble实现一个operator(),就像AllTypesOperatorOverload结构体中一样,就会出现编译错误error: invalid conversion from '...' {aka double ...} to '...' {aka int ...},因此似乎std::variant处理函数签名有所不同?
我尝试使用SFINAE但似乎并没有缓解问题。
struct AllTypesOperatorOverload {
  template<typename T, std::enable_if_t<std::is_same<T, int>::value>>
  T operator()(const T &data) {
    std::cout << "visiting int node" << std::endl;
    return data;
  }
  template<typename T, std::enable_if_t<std::is_same<T, double>::value>>
  T operator()(const T &data) {
    std::cout << "visiting double node" << std::endl;
    return data;
  }
};

现在将报告一个错误:error: no type named 'type' in 'struct std::invoke_result<AllTypesOperatorOverload, int&>'。是否有一种方法可以为所有类型提供operator(),然后根据foo的设置方式,接收其相应的值到bar中,并具有正确的类型?我知道std::get_if<T>()可能在这里是有用的,但理想情况下,除非绝对必要(这是一个简化的示例,我可能希望在我的std::variant中有几个更多的类型),否则不想使用长的if语句检查每个类型。

我想知道问题是否是因为double和int可以相互转换。使用std::variant<int*,std::string>是否会重现此问题? - Mooing Duck
1个回答

10
错误信息很糟糕,但问题在于变量的所有替代方案在访问器中必须具有相同的返回类型。您的AllTypesOperatorOverload没有遵守这个规则,返回了一个double和一个int,这不是相同的类型。
最新版本的libstdc++或任何版本的libc++都会产生更好的错误消息,明确告诉您这一点(以下是我换行的结果)。
error: static_assert failed due to requirement '__visit_rettypes_match'
    "std::visit requires the visitor to have the same return type for
     all alternatives of a variant"
              static_assert(__visit_rettypes_match,

这是有道理的,因为当你看到这行代码时,bar的类型是什么?

auto bar = std::visit(AllTypesOperatorOverload(), foo);

如果允许返回不同类型,bar的类型将取决于foo在运行时所持有的替代方案。这在C++中是行不通的。
请注意,有更简单的方法来为std::visit创建访问者,而不是使用外部定义的结构体。您可以使用if constexpr和lambda表达式:
std::visit([](auto value) {
    if constexpr (std::is_same_v<int, decltype(value)>) {
        std::cout << "visiting int\n";
    } else {
        static_assert(std::is_same_v<double, decltype(value)>);
        std::cout << "visiting double\n";
    }
    std::cout << "bar: " << value << '\n';
}, foo);

或者,你可以定义一个 overloaded 帮助结构体来重载lambda表达式:

template <typename... Lambdas>
struct overloaded : Lambdas...
{
    template <typename... Fns>
    explicit constexpr overloaded(Fns&&... fns)
        : Lambdas(std::forward<Fns>(fns))...
    {}

    using Lambdas::operator()...;
};
template <typename... Lambdas>
overloaded(Lambdas...) -> overloaded<Lambdas...>;

// Usage:
std::visit(overloaded{
    [](int value) {
        std::cout << "visiting int\n";
        std::cout << "bar: " << value << '\n';
    },
    [](double value) {
        std::cout << "visiting double\n";
        std::cout << "bar: " << value << '\n';
    }
}, foo);

oevrloaded 的完整声明只有两行代码... template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; }; template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>; - Michaël Roy
自己注意:gcc编译器错误信息不好,clang好(我知道这点,但有时会忘记检查...)。打印没问题(但这并不是我的问题),我正在尝试获取当前存储在foo中的值(它可以具有在std::variant中指定的任何类型)。有可能实现吗? - tom
1
@tom "我正在尝试获取当前存储在 foo 中的值(它可以是 std::variant 中指定的任何类型)" 问题在于这没有意义。如果你获取了这个值,那么 bar 的类型会是什么呢?变量可能是 intdouble,所以 bar 的类型必须是 intdouble 的叠加。这是不允许的。最接近的方法是将 bar 设为 std::variant<int, double>,但这样我们又回到了起点。相反,你应该将处理 bar 的代码提升到访问者中,在那里它可以知道存储在 foo 中的内容的类型。 - Justin
@Justin:感谢您的澄清,我会看看能否以这种方式使其工作。 - tom

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接