前言
std::variant
(可变体) 是 C++17
中引入的一个新的类模板,提供了一种存储不同类型的值的方式,类似于之前版本中的 union
(联合体),但可以存储非 POD
类型和类对象,能够在运行时进行类型检查和转换,但具有更多的功能和更高的类型安全性,今天来看一下存储在std::variant
中的数据要怎么读取。
variant的简单使用
可以参考cppreference网站的使用示例,也可以看看下面这个例子:
1 |
|
在示例程序中,定义了一个 std::variant
对象 value
来存储整型、浮点型和字符串类型中的任意一种,然后分别将 value
赋值为整型、浮点型和字符串类型,并使用 std::get
来获取对应的值,此时可以正常打印 value
对象中存储的值
当我们试图将 value 赋值为其它未在定义变量时指定的类型时,编译器将会报编译错误,而当我我们试图获取 value 中不存在的类型的值时,程序将会抛出 std::bad_variant_access 异常,可以使用 try-catch 已经捕获。
通过这段代码我们可以得知,使用std::variant可以方便地存储多种类型的数据,并且能够在运行时进行类型检查和转换,这使得代码更加清晰易读,便于维护。
variant相关函数和类
- 成员函数
index
:返回 variant 中保存用类型的索引下标valueless_by_exception
:返回 variant 是否处于因异常导致的无值状态emplace
:原位构造 variant 中的值
- 非成员函数
visit
:通过调用variant保存类型值所提供的函数对象获取具体值holds_alternative
:检查某个 variant 是否当前持有某个给定类型std::get
:以给定索引或类型读取 variant 的值,错误时抛出异常get_if
:以给定索引或类型,获得指向被指向的 variant 的值的指针,错误时返回空指针
- 辅助类
monostate
:用作非可默认构造类型的 variant 的首个可选项的占位符类型(预防一些类型不提供无参构造函数)bad_variant_access
:非法地访问 variant 的值时抛出的异常variant_npos
:非法状态的 variant 的下标
访问std::variant数据
从前面提到的例子和函数说明,我们可以看到有多种方式来访问std::variant数据,接一下来一起总结一下:
std::get搭配index函数使用
1 |
|
先用 index()
查询 variant保存的类型索引,然后在通过 std::get<NUMBER>()
获取其中的值
std::get搭配std::holds_alternative函数使用
1 |
|
先通过 std::holds_alternative()
查询 variant保存的类型,然后在通过 std::get<TYPE>()
获取其中的值
std::get_if函数
1 |
|
直接使用 std::get_if
函数获取对应值的指针,如果类型不匹配会返回空指针
std::visit函数
使用函数visit函数访问时,有点像使用std::sort
这类函数,可以搭配自定义的结构(排序)重写operator()
,让其变成可以被调用的函数对象,也可以定义lambda自带可执行特性。
自定义访问结构的写法
1 |
|
定义lambda函数组重载
1 |
|
这种方式将多个lambda放到一起形成重载,进而达到了访问variant数据的目的。
overloaded是什么
上文例子中的最后一个中使用到了 overloaded
,这令人眼花缭乱的写法着实很诡异,不过我们可以从头来分析一下,最后两个例子中等价的两部分是
1 | struct VisitPackage |
与
1 | template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; }; |
要想理解它们为什么等价,我们首先的得弄清楚lambda表达式是什么,在剖析lambda之前先来看看 std::visit
函数需要的参数是什么,分析std::visit
的参数,先看 struct VisitPackage
结构更容易一些。
std::visit的第一个参数
通俗的来说std::visit
的第一个参数需要的是一个可执行的对象,如果对象能被执行就需要实现 operator()
这个操作符,看起来像函数一样,这就是为什么在 struct VisitPackage
中定义了 operator()
,并且定义了两个形成了参数不同的静态重载,作用就是为了在访问 variant
对象时适配不同的类型,在访问variant
对象时会选择最匹配的 operator()
函数,进而实现了访问variant中不同类型值行为不同的目的。
那lambda表达式能实现这个目的吗?我们接着往下看
lambda 是什么
自从 C++11 引入lambda之后,对它赞美的声音不绝于耳,那lambda表达式究竟是怎样实现的呢?真的就是一个普通的函数吗?我们看一个小例子:
1 | int main() { |
这是一个使用lambda表达式简单的例子,代码中定义了一个int类型参数的返回值也是int的lambda函数,作用就是将外部变量x与函数参数的和返回,我们使用 cppinsights.io 网站来将此段代码展开
1 | int main() |
可以发现我们虽然定义了一个lambda函数,但是编译器为它生成了一个类 __lambda_4_15
,生成了 int&
类型的构造函数,并实现了 operator
操作符,再调用lambda函数时先生成了 __lambda_4_15
类的对象,再调用类的 operator()
函数 func.operator()(7);
,看到这里是不是似曾相识,虽然还不是很明白,但是和struct VisitPackage
的定义总是有种说不清道不明的血缘关系。
弄清楚了lambda函数的本质,现在要实现的是怎么把多个lambda函数合成一个对象,并且让他们形成重载,因为lambda函数本质上存在一个类,只需要定义一个子类,继承多个lambda表达式就可以了,其实 overloaded
这个模板类就是为了实现这个目的。
overloaded剖析
1 | template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; }; |
一时间看起来很难理解,它来自 en.cppreference.com 中介绍 std::visit
访问 std::variant
的例子,可以换行看得更清楚一点:
1 | // helper type for the visitor #4 |
template
struct overloaded : Ts… { using Ts::operator()…; }; 这是一个类模板的声明,模板的名字是overloaded
分步拆解来看:
template
struct overloaded 表示类的模板参数为可变长的参数包 Ts
假设 Ts 包含 T1, T2, … , TN,那么这一句声明可以展开为:templatestruct overloaded struct overloaded : Ts…
表示类的基类为参数包 Ts 内所有的参数类型
假设 Ts 包含 T1, T2, … , TN,那么这一句声明可以展开为:struct overloaded : T1, T2, …, TN{ using Ts::operator()…; };
这是一个函数体内的变长 using 声明
假设 Ts 包含 T1, T2, … , TN,那么这一句声明可以展开为:{ using T1::operator(), T1::operator(), …, TN::operator(); }
经过这步声明,overloaded 类的参数包 Ts 内所有的参数作为基类的成员函数operator()均被 overloaded 类引入了自己的作用域template
overloaded(Ts…) -> overloaded ; 这是一个自动推断向导说明,用于帮助编译器根据 overloaded 构造器参数的类型来推导 overloaded 的模板参数类型,C++17需要,C++20已不必写
它告诉编译器,如果 overloaded 构造器所有参数的类型的集合为Ts,那么 overloaded 的模板参数类型就是 Ts 所包含的所有类型
如果表达式a1, a2, …, an的类型分别为T1, T2, …, TN,那么构造器表达式overloaded x{a1, a2, …, an} 推导出,overloaded的类型就是 overloaded
经过这些解释,我们可以认为在最后一个例子中可能产生了类似这样的代码:
1 |
|
总结
std::variant
可以存储多个类型的值,并且它会自动处理类型转换和内存分配std::variant
可以存储非POD
类型和类对象,能够在运行时进行类型检查和转换,具有更高的类型安全性- 可以使用
std::visit
全局函数来访问std::variant
中存储的值,该函数根据存储的值的类型自动选择调用哪个函数对象 - 可以使用
std::holds_alternative
函数来检查变量中是否存储了特定的类型 - 定义lambda函数时,编译器会为其生成一个类
代码是程序世界的运行规则,一个个字母的敲击正在规定着程序世界的运行秩序,无论代码写的天花乱坠,究其本质还是规定这一步要做什么,下一步继续做什么而已~
2023-6-23 21:13:04