使用std::visit和lambda表达式尝试从std :: variant中返回值

10
假设存在一个名为v的变量,其定义如下:
std::variant<int,char,double,bool,std::string> v;

我希望您能够使用std::visit或std::get获取std::variant的基础值。我尝试了以下方法:
constexpr size_t idx = v.index();
auto k = std::get<idx>(v);

但是后来发现,如果变量v本身不是constexpr,则会失败。即使如此,使用std::string也可能存在问题(由于std::string的析构函数定义)。
我的第二次尝试是尝试做以下操作:
auto k = std::visit([](auto arg){return arg;}, v);

但收到了这个:
$g++ -o main *.cpp --std=c++17
In file included from main.cpp:5:0:
/usr/include/c++/7/variant: In instantiation of ‘static constexpr auto std::__detail::__variant::__gen_vtable_impl<std::__detail::__variant::_Multi_array<_Result_type (*)(_Visitor, _Variants ...)>, std::tuple<_Tail ...>, std::integer_sequence<long unsigned int, __indices ...> >::_S_apply() [with _Result_type = int; _Visitor = main()::<lambda(auto:1)>&&; _Variants = {std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&}; long unsigned int ...__indices = {1}]’:
/usr/include/c++/7/variant:663:61:   required from ‘static constexpr void std::__detail::__variant::__gen_vtable_impl<std::__detail::__variant::_Multi_array<_Result_type (*)(_Visitor, _Variants ...), __dimensions ...>, std::tuple<_Variants ...>, std::integer_sequence<long unsigned int, __indices ...> >::_S_apply_single_alt(_Tp&) [with long unsigned int __index = 1; _Tp = std::__detail::__variant::_Multi_array<int (*)(main()::<lambda(auto:1)>&&, std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&)>; _Result_type = int; _Visitor = main()::<lambda(auto:1)>&&; long unsigned int ...__dimensions = {5}; _Variants = {std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&}; long unsigned int ...__indices = {}]’
/usr/include/c++/7/variant:651:39:   required from ‘constexpr const std::__detail::__variant::_Multi_array<int (*)(main()::<lambda(auto:1)>&&, std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&), 5> std::__detail::__variant::__gen_vtable<int, main()::<lambda(auto:1)>&&, std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&>::_S_vtable’
/usr/include/c++/7/variant:704:29:   required from ‘struct std::__detail::__variant::__gen_vtable<int, main()::<lambda(auto:1)>&&, std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&>’
/usr/include/c++/7/variant:1239:23:   required from ‘constexpr decltype(auto) std::visit(_Visitor&&, _Variants&& ...) [with _Visitor = main()::<lambda(auto:1)>; _Variants = {std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&}]’
main.cpp:89:49:   required from here
/usr/include/c++/7/variant:704:49:   in constexpr expansion of ‘std::__detail::__variant::__gen_vtable<int, main()::<lambda(auto:1)>&&, std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&>::_S_apply()’
/usr/include/c++/7/variant:701:38:   in constexpr expansion of ‘std::__detail::__variant::__gen_vtable_impl<std::__detail::__variant::_Multi_array<int (*)(main()::<lambda(auto:1)>&&, std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&), 5>, std::tuple<std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&>, std::integer_sequence<long unsigned int> >::_S_apply()’
/usr/include/c++/7/variant:641:19:   in constexpr expansion of ‘std::__detail::__variant::__gen_vtable_impl<std::__detail::__variant::_Multi_array<int (*)(main()::<lambda(auto:1)>&&, std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&), 5>, std::tuple<std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&>, std::integer_sequence<long unsigned int> >::_S_apply_all_alts<0, 1, 2, 3, 4>(\xe2\x80\x98result_dec\xe2\x80\x99 not supported by dump_expr#<expression error>, (std::make_index_sequence<5>(), std::make_index_sequence<5>()))’
/usr/include/c++/7/variant:686:43: error: invalid conversion from ‘std::__success_type<char>::type (*)(main()::<lambda(auto:1)>&&, std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&) {aka char (*)(main()::<lambda(auto:1)>&&, std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&)}’ to ‘int (*)(main()::<lambda(auto:1)>&&, std::variant<int, char, double, bool, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&)’ [-fpermissive]
       { return _Array_type{&__visit_invoke}; }
                                           ^

我不知道为什么 std::visit 调用无法工作。 我认为我提供了一个轻松的 lambda 表达式,它接受 variant 的所有可能类型并返回底层值,但似乎我理解错了什么。
我现在想使用 std::variant(最初考虑过 std::any,请参见避免使用 std::any 写重复的类型检查代码),但我需要一种返回包含值的方法。 非常感谢任何帮助。 谢谢。

2
你正在尝试实现k的类型在运行时确定为其中一种变量。C++是强类型语言,所以你不能这样做。 - Öö Tiib
https://dev59.com/blYN5IYBdhLWcg3wHU69 - macroland
2
表达式的类型必须在编译时知道,而包含的变体类型是运行时属性;因此您不能像这样做任何事情。相反,您需要为每个可能包含的类型指定要执行的操作。 - M.M
3
你打算如何处理变量 k?你能展示一些代码吗,哪怕是无法运行的,但要有意义的东西? - n. m.
2
这个问题从根本上讲没有意义。你不是取出值,而是访问它。 - Barry
显示剩余2条评论
3个回答

11
您所尝试的方法行不通,因为在运行时,对象variant持有的类型已知,而您想要存储它的变量的类型必须在编译时已知。处理这个variant的模式是通过一个可以处理任何类型的模板函数来完成工作,或者拥有一组可以接受来自variant的任何类型的重载函数。
选项1
在模板函数上进行所有工作:
std::visit([] (const auto& k) { std::cout << k; }, v);

或者,在函数内部使用constexpr if进行区分。但是我认为这种方法没有必要,因为有更好的选择,即使用重载(见下文):
std::visit([] (const auto& k) {
        using T = std::decay_t<decltype(k)>;
        if constexpr (std::is_same_v<T, int>)
            std::cout << "int with value " << k << '\n';
        else if constexpr (std::is_same_v<T, char>)
            std::cout << "char with value " << k << '\n';
        else if constexpr (std::is_same_v<T, double>)
            std::cout << "double with value " << k << '\n';
        else if constexpr (std::is_same_v<T, std::string>)
            std::cout << "std::string with value " << k << '\n';
    }, v);

选项二

调用不同的重载函数

template <class... Fs> struct Overload : Fs... { using Fs::operator()...; };
template <class... Fs> Overload(Fs...) -> Overload<Fs...>;

std::visit(
    Overload{
        [] (int k) { /* deal with k here */ },
        [] (char k) { /* deal with k here */ },
        [] (double k) { /* deal with k here */ },
        [] (bool k) { /* deal with k here */ },
        [] (std::string k) { /* deal with k here */ }
    },
    v
);

10
我想通过使用std::visit或std::get从std :: variant中获取底层值。
如果您确实希望保持底层当前值,则必须使访问支持对每个可能的值进行特定处理。例如,像这样:
std::visit([] (const auto& var) {
    if constexpr (std::is_same_v<std::decay_t<decltype(var)>, int>) {
        // Do something with var of type int
    }
    else if constexpr (std::is_same_v<std::decay_t<decltype(var)>, char>) {
        // Do something with var of type char
    }
    else if constexpr (std::is_same_v<std::decay_t<decltype(var)>, double>) {
        // Do something with var of type double
    }
    else if constexpr (std::is_same_v<std::decay_t<decltype(var)>, bool>) {
        // Do something with var of type bool
    }
    else if constexpr (std::is_same_v<std::decay_t<decltype(var)>, std::string>) {
        // Do something with var of type std::string
    }
}, v);

这是因为C++是一种静态类型语言,也就是说,在编译时必须知道所有变量的类型。因此,当你想要获取可能包含在std::variant中的各种类型之一作为当前值时,编译器不能允许你只声明auto并完成它。

...但我需要一种方法来返回包含的值。

在C++中,由于是静态类型,没有办法在不经过可能情况的情况下执行此操作。如果你想要一个调用,它接受这样一个std::variant实例并返回一个std::string,那么可以修改上面的代码,对于上述if/else语句中的每个情况都返回std::to_string(var)

  • 请注意,这里使用了constexpr if关键字。如果不清楚为什么需要这样做,值得阅读一下。使用std::decay_t模板是必要的,以确保在std::is_same_v模板中比较的类型是基本类型(非const和非引用限定符)。

摘自评论:

这是因为你并没有试图将基础类型的变量赋值/复制到自己的变量中。这又一次需要在编译期间知道这样一个变量的类型。但是当 std::variant 被要求提供其当前持有值的字符串表示形式--例如由于 std::cout 的运算符 <<--时,内部所做的与我们上面的 if-else 开关具有相同的语义,即针对此 variant 实例的每种可能的基础类型进行不同的处理。 澄清:显然,有多种方式可以指定处理std::variant实例当前可能持有的不同可能性。例如,如std::visit cppreference页面所示,您可以使用基于模板推导指南std::visit(overloaded { ...方式来完成它,虽然这种方式可以产生更好、更短的代码,但需要一些更深入的解释才能理解其机制,我认为,它包括从lambda继承等其他事情,因此我认为它超出了本回答的说明范围,就我理解所提出的问题而言。您可以在这里这里阅读有关它的所有内容。或者更容易地,在本问题的另一个回答中查看用法代码示例。

关于编译错误:这段代码可以正常编译,但并没有达到你想要的效果:

using your_variant = std::variant<int,char,double,bool,std::string>;
your_variant v;

auto k = std::visit([](auto arg)-> your_variant {return arg;}, v);

你的代码未能编译成功,因为lambda表达式需要显式声明返回类型 {{-> your_variant}},编译器无法从表达式中推断出来。
解决相同问题的另一种有效语法是只声明参数类型,这样编译器就可以知道它返回的内容,就像函数返回{{auto}}一样。
auto k2 = std::visit([](your_variant arg) {return arg;}, v);

这样做的编译问题是:
constexpr size_t idx = v.index();
auto k = std::get<idx>(v);

由于静态类型v 可以在运行时持有任何单个索引,而 std::get()模板参数需要在编译时确定。


1
@markf78:“为什么std::visit([](auto&& arg){std::cout << arg;}, v);能够工作?” 因为lambda参数列表中的auto表示一个模板。在std::visit内部有一个switch或等效结构,每个分支都调用不同实例化的模板。 - n. m.
第一个例子与 OP 无关,因为它不返回任何值。可以将其更改为将每个选项转换为一个类型的值,例如 std::string。 - Öö Tiib
我的意思是,如果 OP 想要使用 variant 的 visit 方法获取一个值,那么它必须是一定类型的,比如字符串,否则仍然是 variant、union 或 any 类型,因此仍然需要根据类型进行 if else 链的判断。 - Öö Tiib
@Holger,我同意开头的例子可能会无意中将糟糕的实践当作建议推荐——因为你指出了冗余的提取->重新包装的误用。我现在已经修复了它,希望你会认同当前的形式。我一开始避免写成这样的原因是必须使用 constexpr if,这需要理解它本身的知识——而对我来说重要的是再次强调静态类型。我现在觉得更好的选择不是回避它,而是像我在编辑中所做的那样做出注释。谢谢。 - Geezer

2

在给定的C++函数中,每个变量都有一个单一且固定的类型。

auto k = std::visit([](auto arg){return arg;}, v);

在这里,你希望k具有多种不同类型之一。C++不支持这种操作。

但是,你可能会问:

std::visit([](auto arg){std::cout << arg;}, v);

工作?在Lambda中,arg可以使用许多不同的类型!

这是因为[](auto arg){...}不是单个函数,而是一个模板函数的简写。模板函数不是函数,而是用于创建函数的模板。

该代码会创建N个不同的函数,每个函数都有一个不同的auto arg类型。它们都被编译。然后,std::visit选择其中一个运行。

std::variant是我们如何在一个变量中存储多种不同可能类型的数据。它具有固定的类型,但它暴露了visit,所以您可以安全地获取底层数据的类型。

现在情况并不那么糟糕。您可以将您的代码放在lambda中

所以,与其:

auto k = std::visit([](auto arg){return arg;}, v);
// code using k

执行以下操作:

std::visit([](auto k){
  // code using k
}, v);

如果您想要“返回一个值”,则必须回到std::variant的领域。假设您想要返回std::vector<T>,其中T是变量中的类型。
template<class...Ts>
using var_of_vec = std::variant< std::vector<Ts>... >;
using my_vector = var_of_vec<int,char,double,bool,std::string>;

my_vector v =std::visit([](auto k)->my_vector{
  std::vector<decltype(k)> vec;
  // code using k
  return vec;
}, v);

在lambda函数体内,您使用单个向量,然后返回向量的变体。


using语句无法编译。 - markf78

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