基础概念
std::variant与c++17版本引入,这是一个类型安全的联合体(union),可以存储多个不同类型中的任意一个。简单理解可以把std::variant认为是一个联合体(union)。在c++17引入这个特性之前,类似的功能只能使用联合体union来实现,但是联合体存在一些弊端:
- 联合体无法知道当前存储的是哪个类型
- union不会调用构造函数和析构函数
- 可能导致未定义的行为
c++针对联合体的这些弊端推出了新的特性std::variant,通过这个特性可以创建一个对象,这个对象完全具备原有的联合体(union)的功能,并且解决了联合体存在的弊端。总结如下:
- std::variant是一个类模板,通过这个类模板可以创建变量,这个变量具有与联合体一样的功能
- std::variant对象的长度相对于联合体而言会稍长一些,因为除了包含最长对象的长度外,对象中还包含了一些控制信息
- std::variant支持自动调用类的构造函数与析构函数
- 虽然有了一些不同,但是使用方法与联合体基本一致,只是对比联合体功能更强大了
- 由于std::variant是一个类,其中包含了一些常用方法来访问这个对象中的相关信息
- 与联合体一样,同一时间std::variant仅存储一个数据类型的数据。
依赖的头文件:#include <variant>
基本使用方法
下面展示一些std::variant的基本使用方法:
#incldue <variant>
#include <iostream>
#include <string>
int main()
{
//创建一个可以存储int string double的变量
std::variant<int, std::string, double> data;
data = 42; //现在存储的是整型
data = "hello"; //现在存储的是字符串类型
data = 3.14; //现在存储的是double
//访问存储的数据的方法
//方法一,通过std::get访问
try
{
double d = std::get<double>(data); //正确,data最后存储的是double
std::cout <<"value = <<d<<std::endl;
//此处会在运行时抛出异常,因为最后的存储的是double,不是int,类型不正确无法获取,这点与联合体有区别
int i = std::get<int>(data);
}
catch(const std::bad_variant_access& e)
{
std::cout <<"错误类型访问:"<<e.what()<<std::endl;
}
//方法二,使用std::get_if方法,这个方法返回的是指针
if(auto pval = std::get_if<double>(&data))
{
std::cout<<"value = "<<*pval<<std::endl;
}
//若没有知道则返回空指针
else
{
std::cout << "不包含double值"<<std::endl;
}
//方法三,std::holds_alternative 检查当前存储的类型,该方法返回布尔型
if(std::holds_alternative<double>(data))
{
cout<<"当前保存的是double"<<std::endl;
}
//方法四:std::visit 访问者模式
std::visit([](const auto& value){
std::cout<<"value = "<<value<<std::endl;
},data)
}
常见的应用场景
思考:联合体(union)为什么不支持自动调用构造函数和析构函数?
假设有如下的联合体定义:
class MyClass
{
private:
int a;
double b;
std::string str;
...
}
union MyUnion {
int x;
std::string str; // 包含非基本类型的 union
Myclass myclass; //自定义类型
};
由于联合体要按照数据成员的最大者申请空间,联合体的所有成员共享这片内存,现在上面的例子中有两个类,那么问题来了:
- 联合体应该按照哪个类的构造函数去构造?
- 如果每个这样的成员都构造一遍,会发生什么?
结果是:
- 编译器不知道构造联合体中那个成员
- 如果把类中所有非基本类型的成员都构造一遍,那么后构造的成员就会破坏之前构造的成员
所以我们一般不在联合体中使用非基本数据类型,如果一定要使用,就需要手动管理声明周期。
#include <string>
union MyUnion {
int x;
std::string str;
// 默认构造函数
MyUnion() : x(0) {} // 默认构造 int
// 析构函数
~MyUnion() {} // 注意:这里不能自动析构 string,因为不知道当前是否存储的是 string
// 禁用拷贝
MyUnion(const MyUnion&) = delete;
MyUnion& operator=(const MyUnion&) = delete;
};
int main() {
MyUnion u;
// 手动构造 string
new (&u.str) std::string("Hello");
// 使用 string
std::cout << u.str << std::endl;
// 手动析构 string
u.str.~basic_string();
// 现在可以使用 int
u.x = 42;
}
这个问题在c++17引入std::variant特性引入后,就存在联合体存在的问题的了,c++在std::variant自动支持非基本数据类型了。
思考:std::variant为什么是类型安全的?
编译时检查
在编译阶段会进行类型检查
std::variant<int, double> data;
data=1; //正确
data = 3.14; //正确
data = "hello"; //编译不通过
运行时检查
当通过一些方法从std::variant中获取值时,会抛出异常,而不会发生未定义的行为
std::variant<int, std::string> v = 42;
// 以下代码会抛出异常,而不是未定义行为
try {
std::string s = std::get<std::string>(v); // 抛出异常,因为当前存储的是int
} catch(const std::bad_variant_access&) {
std::cout << "类型访问错误" << std::endl;
}