再谈c++中的variant和visit

一、std::variant

前面把它和volatile对比说明了一下。本文重点说一下他的应用,特别是和它配合的std::visit一起来阐述一下std::variant的用法。optional是从有和无来选择,而variant是在存在的几个类型里任选一个。std::any呢?是一个包含任何类型的单值的类型安全容器。前两个都需要声明类型的范围,后面这个不用。明白了吧。
“类模板 std::variant 表示一个类型安全的联合体。”这句话有点意思,那它和union区别是什么?union是从C时代就有的一个联合体,但是,对象是无法确定到底这个联合体的具体类型。所以在union中产生了很多小技巧,可以让数据来回转换,达到一种数据类型的莫名的变化。可在std::variant这个一旦赋值,对象是知道这个数据类型的;另外,union无法继承,这个倒不是什么多大点的事儿;早期的union只是平凡的数据类型,在c++11后在一些特定条件下,也可以是非平凡的数据。
继续向下看吧,就明白了。

二、应用

1、声明和使用
对std::variant来说,它的声明使用和普通的模板没有什么区别:

//声明
std::variant<int, float,std::string> a,b;
//赋值
a = 10;
b = "test";
a = 1.0f;

//获取值
float tmp = std::get<float>(a);
std::string s = std::get<2>(b);

可以看到在std::variant可以通过类型和索引来获取指定的值。那要是没有这个类型或者索引超范围会是什么样子呢:

int d = std::get<int>(a);
s = std::get<3>(b);

如果索引越界,编译无法通过。但是如果把索引值改了,编译可以通过,这样操作会抛出一个std::bad_variant_access异常。一般来说在c++里尽量少处理异常,所以std::variant提供了一个接口std::get_if:

int *d = std::get_if<int>(&a);
std::string *ss = std::get_if<2>(&b);

std::cout << "d p is:" << d << "  ss p is:" << ss << std::endl;

打印的结果前者是个0值,后者是正常的指针值。

2、 std::visit
看一下它的定义:

template <class Visitor, class... Variants>
constexpr /*see below*/ visit(Visitor&& vis, Variants&&... vars);(1)	(C++17 起)
template <class R, class Visitor, class... Variants>
constexpr R visit(Visitor&& vis, Variants&&... vars) (2)	(C++20 起)


vis: 接受每个 variant 的每个可能可选项的可调用 (Callable) 对象
vars: 传递给观览器的 variant 列表

它就是针对std::variant的更形式化的访问接口。Visitor其实就是一个访问器,也可以理解是一个函数。

#include <iomanip>
#include <iostream>
#include <string>
#include <type_traits>
#include <variant>
#include <vector>

// 要观览的 variant
using var_t = std::variant<int, long, double, std::string>;

// 观览器 #3 的辅助常量
template<class> inline constexpr bool always_false_v = false;

// 观览器 #4 的辅助类型
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
// 显式推导指引( C++20 起不需要)
template<class... Ts> overloaded(Ts...)->overloaded<Ts...>;

void VisitVar()
{
    std::vector<var_t> vec = { 10, 15l, 1.5, "hello" };
    for (auto&& v : vec) {
        // 1. void 观览器,仅为其副效应调用
        std::visit([](auto&& arg) {std::cout << arg; }, v);

        // 2. 返回值的观览器,返回另一 variant 的常见模式
        var_t w = std::visit([](auto&& arg) -> var_t {return arg + arg; }, v);

        std::cout << ". After doubling, variant holds ";
        // 3. 类型匹配观览器:亦能为带 4 个重载的 operator() 的类
        std::visit([](auto&& arg) {
            using T = std::decay_t<decltype(arg)>;
            if constexpr (std::is_same_v<T, int>)
                std::cout << "int with value " << arg << '\n';
            else if constexpr (std::is_same_v<T, long>)
                std::cout << "long with value " << arg << '\n';
            else if constexpr (std::is_same_v<T, double>)
                std::cout << "double with value " << arg << '\n';
            else if constexpr (std::is_same_v<T, std::string>)
                std::cout << "std::string with value " << std::quoted(arg) << '\n';
            else
                static_assert(always_false_v<T>, "non-exhaustive visitor!");
            }, w);
    }

    for (auto&& v : vec) {
        // 4. 另一种类型匹配观览器:有三个重载的 operator() 的类
        // 注:此情况下 '(auto arg)' 模板 operator() 将绑定到 'int' 与 'long' ,
        //    但若它不存在则 '(double arg)' operator() *亦将* 绑定到 'int' 与 'long' ,
        //    因为两者均可隐式转换成 double 。使用此形式时应留心以正确处理隐式转换。
        std::visit(overloaded{
            [](auto arg) { std::cout << arg << ' '; },
            [](double arg) { std::cout << std::fixed << arg << ' '; },
            [](const std::string& arg) { std::cout << std::quoted(arg) << ' '; },
            }, v);
    }
}
int main()
{
    VisitVar();
    return 0;
}

3、对对象的支持
在官网还是一般的例程里,都只是对默认类型进行举例说明。其实std::variant是支持对对象操作的,包括继承等。看一下下面的例子:

class A
{
public:
   // A() { std::cout << "call default!" << std::endl; }
    A(int d) { d_ = d; }
private:
    int d_ = 0;
};
void UseObject()
{
    A a;
    std::variant<A, int> v0;
    std::variant<A, int> v1{ a };

}
int  main()
{
    UseObject();
    return 0;
}

如果没有默认构造函数或者显示的声明默认构造函数,那么这个是编译不过去的,上面的这个,打开注释就没有问题了。同样,如果把A移到非第一位,也就没问题了。不过在c++中考虑到了这个问题,也就是有强迫非要放在第一位,非要没有默认构造函数(无语,真有如此之人),那么可以用std::monostate来搞定,看它的例程:

void UseObject()
{
    std::variant<std::monostate,A> v0;
    std::cout << "A index:" << v0.index() << std::endl;//此处为0
}
int  main()
{
    UseObject();
    return 0;
}

std::monostate其实是做了一个替补位。但其实如果给这个变量赋值一个A的变量,index就会变成1.这说明这里只是一个编译通过的占位符。“有意为行为良好的 std::variant 中空可选项所用的单位类型。具体而言,非可默认构造的 variant 可以列 std::monostate 为其首个可选项:这使得 variant 自身可默认构造。”

三、总结

对比学习,对比操作会更深刻的体会每一个类和接口的不同,特别是一些细节。正如总是说:“魔鬼在于细节”正是如此。为什么出这几个类?为什么会有操作这些类的接口?可以看一下一些更高级的语言,他们对变量的定义是怎么定义的。比如JS里,变量不定义都可以使。虽然它和c++等的强类型语言有本质的不同,但一些上层的应用可不可以引进一些可以实现的变化,这才是重点。和初学者大谈什么底层,没有意义,只会增加他们对这门语言的畏难情绪。把一些底层和细节封装起来,让初学者更容易接受,更容易使用,这才是王道。
语言再好,没人用,最终也会消亡。不要总沉迷于技术的底层,对于一门语言的发展来说,那些都可以暂时放一放!

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值