C++ 可变体(variant)
迪丽瓦拉
2025-06-01 21:27:34
0

一、可变体(variant) 基础用法

Union的问题:

  • 无法知道当前使用的类型是什么。
  • 而且union无法自动调用底层数据成员的析构函数。
  • 创建复杂的数据类型的封装能力非常鸡肋.

variant

C++17 提供了 std::variant

可变体的声明

下面的代码是声明一个可变体的用法,在variant关键字的尖括号内,依次指定可变体的的数据类型。在可变体的内部,这些数据类型存在顺序关系

int main()
{//声明一个可变体的对象std::variant tmp;
}

可变体的辅助函数

C++17标准中还提供了一些常用可变体的辅助函数模板的API

  • std::variant_size_v——用于检测可变体内部可切换的数据类型的个数
int main()
{//声明一个可变体的对象std::variant tmp;static_assert(std::variant_size_v == 3);   // static_assert静态断言,如果表达式为false会在编译时报错  
}
  • std::visit——用于访问可变体中的当前处于活动状态的数据类型的实例(即当前在使用的类型实例)
  • index方法返回当前可变体内部对应的数据类型的索引
#includestruct PrintVisitor  {  //visitorvoid operator()(int i) {std::cout << "int: " << i << '\n';}void operator()(double i) {std::cout << "double: " << i << '\n';}void operator()(std::string i) {std::cout << "string: " << i << '\n';}
};int main()
{std::variant tmp;static_assert(std::variant_size_v == 3);// default initialized to the first alternative, should be 0std::visit(PrintVisitor {}, tmp);std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;tmp = 100.00;std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;std::visit(PrintVisitor {}, tmp);tmp = "hello super world";std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;std::visit(PrintVisitor {}, tmp);}
  • 当对可变体赋值的数据类型是float,那么可变体对象tmp内部就会自动切换为float。
  • 当对可变体赋值的数据类型是string,那么可变体对象tmp内部就会自动切换为string。

在这里插入图片描述

std::visit简单来说就是;用来给可变体内的每一个数据类型添加上相应的动作,例如:

#includestruct PrintVisitor {  //visitorvoid operator()(int i) {std::cout << "int: " << i << '\n';}void operator()(double i) {std::cout << "double: " << i << '\n';}void operator()(std::string i) {std::cout << "string: " << i << '\n';}
};int main()
{std::variant value = "123";static_assert(std::variant_size_v == 3, "error");std::visit(PrintVisitor{}, value);return 0;
}

在这里插入图片描述
还有一种更为高效的方式:

#includeint main()
{std::variant value = 1.123;static_assert(std::variant_size_v == 3, "error");std::visit([](auto &&arg) {//using C++17提供的重命名using T = std::decay_t;  // 类型退化,去掉类型中的const 以及 &if constexpr(std::is_same_v) {     //编译时if,只有被选中的if constexpr分支才会被实例化。std::cout << "int: " << arg << '\n';} else if constexpr(std::is_same_v) {   //std::is_same_v:判断输入的类型是否是指定的模板类型std::cout<< "double: "<< arg <<'\n';} else if constexpr(std::is_same_v) {std::cout<< "string: "<< arg <<'\n';}}, value);return 0;
}

在这里插入图片描述
这种方式高效的原因在于它是在编译期完成的类型判断。

std::visit的参数列表是不定长的,可以传入多个variant变量:

template 
constexpr visit(Visitor&& vis, Variant&&... vars);
  • std::get_if和std::get的区别
    两个方法的参数都可以是index(下标)或者T(类型)。
    当外部代码尝试获取可变体对应的数据类型的值,那么使用 std::get_if 或std::get 访问该数据类型的值(但这可能会引发bad_variant_access 异常)。通常get_if保证std::get在访问可变体时不会抛出bad_variant_access 异常,提供了访问前的类型安全判断

  • hold_alternative<> —— 判断可变体当前持有的数据类型

#includestruct PrintVisitor  {  //visitorvoid operator()(int i) {std::cout << "int: " << i << '\n';}void operator()(double i) {std::cout << "double: " << i << '\n';}void operator()(std::string i) {std::cout << "string: " << i << '\n';}
};int main()
{std::variant tmp;static_assert(std::variant_size_v == 3);// default initialized to the first alternative, should be 0std::visit(PrintVisitor {}, tmp);std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;tmp = 100.0f;std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;std::visit(PrintVisitor {}, tmp);tmp = "hello super world";std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;std::visit(PrintVisitor {}, tmp);//当前tmp存的是string类型值if(const auto intPtr (std::get_if(&tmp)); intPtr)    //intPtr不为真,所以不会执行std::cout << "int! " << *intPtr << '\n';if(const auto doublePtr (std::get_if(&tmp)); doublePtr)   //doublePtr不为真,所以不会执行std::cout << "int! " << *doublePtr << '\n';if(std::holds_alternative(tmp))std::cout << "可变体持有int类型\n";else if(std::holds_alternative(tmp))std::cout << "可变体持有double类型\n";else if(std::holds_alternative(tmp))std::cout << "可变体持有string类型\n";
}

在这里插入图片描述

  • 访问可变体的异常处理

为了给可变体的访问增强类型安全,在上下文可以增加bad_variant_access的异常检测。下面是一个异常处理的示例。由于当前的可变体对象内部活动类型是string。因此尝试get< double>(tmp)、get< 0 >(tmp)、get< 1 >(tmp)这类的访问操作都会抛出bad_variant_access异常。

#includestruct PrintVisitor  {  //visitorvoid operator()(int i) {std::cout << "int: " << i << '\n';}void operator()(double i) {std::cout << "double: " << i << '\n';}void operator()(std::string i) {std::cout << "string: " << i << '\n';}
};int main()
{std::variant tmp;static_assert(std::variant_size_v == 3);// default initialized to the first alternative, should be 0std::visit(PrintVisitor {}, tmp);std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;tmp = 100.0f;std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;std::visit(PrintVisitor {}, tmp);tmp = "hello super world";std::cout << "可变体的活动类型返回的index:" << tmp.index() << std::endl;std::visit(PrintVisitor {}, tmp);//当前tmp存的是string类型值if(const auto intPtr (std::get_if(&tmp)); intPtr)    //intPtr不为真,所以不会执行std::cout << "int! " << *intPtr << '\n';if(const auto doublePtr (std::get_if(&tmp)); doublePtr)   //doublePtr不为真,所以不会执行std::cout << "int! " << *doublePtr << '\n';if(std::holds_alternative(tmp))std::cout << "可变体持有int类型\n";else if(std::holds_alternative(tmp))std::cout << "可变体持有double类型\n";else if(std::holds_alternative(tmp))std::cout << "可变体持有string类型\n";try{/* code */auto f = std::get(tmp);std::cout << "double! " << f << '\n';}catch(std::bad_variant_access&){std::cout << "可变体内部当前持有的数据类型和get<>的传入参数类型不一致" << '\n';}
}

在这里插入图片描述

小结:

  • 可通过hold_alternative当前使用的类型。
  • 可变体不允许获取非活动类型的值。
  • 可变体不会发生额外的堆内存分配。
  • 可以使用std::visit对当前保留类型调用某些操作。
  • 没有通过赋值的初始化可变体,则可变体默认使用声明中的第一种类型来初始化可变体,在这种情况下,第一个声明的类型必须具有默认构造函数

二、可变体(variant)的初始化

variant的构造

针对聚合类型的variant构造

下面代码定义了ItCat这个类,并且在声明可变体的第一个类型参数就是ItCat,不用问这段代码报错的原因其实很简单,因为ItCat没有显式提供默认的构造器。
在这里插入图片描述
那么给他ItCat这个用户自定义类型加一个默认构造器,那么在可变体在初始化过程中,就能从类型参数列表中的第一个ItCat获得一个默认构造器。
在这里插入图片描述

可变体的类型模糊的传参构造

#includeclass ItCat {
public:ItCat()=default;ItCat(int, float) {}
};int main()
{std::variant tmp = 1.34;std::cout << tmp.index() << '\n';
}

在这里插入图片描述

对于可变体声明中的参数列表,int、float、double它们可相互转换的数据类型,但对于强调类型安全的C++编译器来说,无疑是给它增加困扰,而C++编译器对待这种模棱两可的值,它默认匹配值的数据类型是确保值的最大精度。因此C++编译器会让可变体选择中的double类型。

std::monostate

为了支持第一个类型没有默认构造函数的variant对象,提供了一个特殊的helper类型:std::monostate。类型std::monostate的对象总是具有相同的状态,因此,它们总是相等的。它自己的目的是表示另一种类型,这样variant就没有任何其他类型的值。也就是说,std::monostate可以作为第一种替代类型,使变体类型默认为可构造的。

std::variant v2; // OK
std::cout << "index: " << v2.index() << '\n'; // prints 0

std::in_place_index函数接口

为了解决传值无棱两可的问题,C++17的的API库提供了std::in_place_index函数接口。下面是使用例子:

#includeclass ItCat {
public:ItCat()=default;ItCat(int, float) {}
};int main()
{std::variant tmp(std::in_place_index<2>, 1.34);std::cout << tmp.index() << '\n';
}

在这里插入图片描述

容器级别传值的variant构造

对于容器级传参的variant初始化问题,就必须显式调用std::in_place_index告知可变体对象要在内置启用哪一个数据类型来构造可变体对象的实例。如下:

#includeclass ItCat {
public:ItCat()=default;ItCat(int, float) {}
};int main()
{std::variant, double> tmp(std::in_place_index<2>, {1, 2, 3, 4, 5});std::cout << std::get>(tmp).size() << '\n';
}

小结:

默认情况下,变体对象使用第一种类型进行初始化,如果类型没有默认构造函数的情况下,会得到一个编译器错误。在这种情况下,应使用 std::monostate 将其作为第一种类型传递。

三、可变体内对象成员的生命周期和访问者模式

修改可变体的对象成员

  • 方式1:赋值操作符
  • 方式2:通过get方法获取真正的对象,然后修改
  • 方式3:通过原地索引API匹配数据类型,然后构造传值达到修改值的目的。
#includeclass ItCat {
public:ItCat()=default;ItCat(int, float) {}
};int main()
{using Mixtype = std::variant, std::string, double>;Mixtype tmp;//方式1:赋值操作符tmp=12;   //此时为intstd::cout << tmp.index() << '\n';std::cout<< std::get<1>(tmp) << '\n';tmp = 23.5; //此时为doublestd::cout<< std::get<4>(tmp) << '\n';//方式2:通过get方法获取真正的对象,然后修改std::get<4>(tmp) = 3011.7;std::cout<< std::get<4>(tmp) << '\n';//方式3:通过原地索引API构造传值tmp = Mixtype(std::in_place_index<2>, {42, 74, 25, 36});for(int i = 0; i < std::get<2>(tmp).size(); ++ i) std::cout << std::get<2>(tmp)[i] << " ";std::cout << '\n';std::get<2>(tmp)[0] = 1024;  //对容器内的单个值进行修改for(int i = 0; i < std::get<2>(tmp).size(); ++ i) std::cout << std::get<2>(tmp)[i] << " ";
}

在这里插入图片描述

  • 方法4:emplace方法赋值。每个可变对象内置了emplace方法,下面是一个具体的例子:
    方法4的缺点是修改可变体内部容器对象时无法对单个元素的值做精准修改
#includeclass ItCat {
public:ItCat()=default;ItCat(int, float) {}
};int main()
{using Mixtype = std::variant, std::string, double>;Mixtype tmp;//方式1:赋值操作符tmp=12;   //此时为intstd::cout << tmp.index() << '\n';std::cout<< std::get<1>(tmp) << '\n';tmp = 23.5; //此时为doublestd::cout<< std::get<4>(tmp) << '\n';//方式2:通过get方法获取真正的对象,然后修改std::get<4>(tmp) = 3011.7;std::cout<< std::get<4>(tmp) << '\n';//方式3:通过原地索引API构造传值tmp = Mixtype(std::in_place_index<2>, {42, 74, 25, 36});for(int i = 0; i < std::get<2>(tmp).size(); ++ i) std::cout << std::get<2>(tmp)[i] << " ";std::cout << '\n';std::get<2>(tmp)[0] = 1024;  //对容器内的单个值进行修改for(int i = 0; i < std::get<2>(tmp).size(); ++ i) std::cout << std::get<2>(tmp)[i] << " ";std::cout << '\n';tmp.emplace<2>({0, 1, 2, 3, 4});  //替换下标为2的对象值for(int i = 0; i < std::get<2>(tmp).size(); ++ i) std::cout << std::get<2>(tmp)[i] << " ";
}

在这里插入图片描述

可变体的对象成员的生命周期

union无法支持其对象成员状态的自动化管理,因此必须手动调用构造函数或析构函数这很容易令程序员写出一大堆屎山代码。std::variant自动化解决对象成员的生命周期。 这意味着如果要切换当前存储对象的数据类型,则variant在切换类型之前,会调用底层类型的析构函数。下面这个示例,很好地解析了这些。

每次对可变体赋值,一旦赋值的数据类型会当前的数据类型不一致,可变体在赋值之前,它内部自动完成对当前持有的对象所占内存的垃圾回收。

std::variant 的访问者模式

std::variant 有一个重要的辅助函数接口 std::visit,这个API可以实现一个甚至多个可变体对象以引用的方式传递给,std::visit回调的函数,而这回调函数就是所谓的“访问者”,以实现一些非常复杂的业务逻辑。

下面是访问者模式的函数模板声明:

/*** @tparam Vistor      访问者函数,即visit的回调函数的函数指针* @tparam Variants    传入参数,一个或多个可变体对象的类型* @param visitor      访问者函数,即visit的回调函数* @param vars         传入参数,一个或多个可变体对象* @return constexpr auto 返回值
*/template
constexpr auto visit(Vistor&& visitor, Variants&&... vars);

visit的使用可以看如下例子:

#includeint main()
{std::variant value = 1.123;static_assert(std::variant_size_v == 3, "error");std::visit([](auto &&arg) {    //arg就是拿到的value中存的值//using C++17提供的重命名using T = std::decay_t;  // 类型退化,去掉类型中的const 以及 &,拿到arg的类型if constexpr(std::is_same_v) {     //编译时if,只有被选中的if constexpr分支才会被实例化。std::cout << "int: " << arg << '\n';} else if constexpr(std::is_same_v) {   //std::is_same_v:判断输入的类型是否是指定的模板类型std::cout<< "double: "<< arg <<'\n';} else if constexpr(std::is_same_v) {std::cout<< "string: "<< arg <<'\n';}}, value);return 0;
}

相关内容

热门资讯

linux入门---制作进度条 了解缓冲区 我们首先来看看下面的操作: 我们首先创建了一个文件并在这个文件里面添加了...
C++ 机房预约系统(六):学... 8、 学生模块 8.1 学生子菜单、登录和注销 实现步骤: 在Student.cpp的...
A.机器学习入门算法(三):基... 机器学习算法(三):K近邻(k-nearest neigh...
数字温湿度传感器DHT11模块... 模块实例https://blog.csdn.net/qq_38393591/article/deta...
有限元三角形单元的等效节点力 文章目录前言一、重新复习一下有限元三角形单元的理论1、三角形单元的形函数(Nÿ...
Redis 所有支持的数据结构... Redis 是一种开源的基于键值对存储的 NoSQL 数据库,支持多种数据结构。以下是...
win下pytorch安装—c... 安装目录一、cuda安装1.1、cuda版本选择1.2、下载安装二、cudnn安装三、pytorch...
MySQL基础-多表查询 文章目录MySQL基础-多表查询一、案例及引入1、基础概念2、笛卡尔积的理解二、多表查询的分类1、等...
keil调试专题篇 调试的前提是需要连接调试器比如STLINK。 然后点击菜单或者快捷图标均可进入调试模式。 如果前面...
MATLAB | 全网最详细网... 一篇超超超长,超超超全面网络图绘制教程,本篇基本能讲清楚所有绘制要点&#...
IHome主页 - 让你的浏览... 随着互联网的发展,人们越来越离不开浏览器了。每天上班、学习、娱乐,浏览器...
TCP 协议 一、TCP 协议概念 TCP即传输控制协议(Transmission Control ...
营业执照的经营范围有哪些 营业执照的经营范围有哪些 经营范围是指企业可以从事的生产经营与服务项目,是进行公司注册...
C++ 可变体(variant... 一、可变体(variant) 基础用法 Union的问题: 无法知道当前使用的类型是什...
血压计语音芯片,电子医疗设备声... 语音电子血压计是带有语音提示功能的电子血压计,测量前至测量结果全程语音播报࿰...
MySQL OCP888题解0... 文章目录1、原题1.1、英文原题1.2、答案2、题目解析2.1、题干解析2.2、选项解析3、知识点3...
【2023-Pytorch-检... (肆十二想说的一些话)Yolo这个系列我们已经更新了大概一年的时间,现在基本的流程也走走通了,包含数...
实战项目:保险行业用户分类 这里写目录标题1、项目介绍1.1 行业背景1.2 数据介绍2、代码实现导入数据探索数据处理列标签名异...
记录--我在前端干工地(thr... 这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 前段时间接触了Th...
43 openEuler搭建A... 文章目录43 openEuler搭建Apache服务器-配置文件说明和管理模块43.1 配置文件说明...