Modern C++ std::variant的实现原理
前言
std::variant是C++17标准库引入的一种类型,用于安全地存储和访问多种类型中的一种。它类似于C语言中的联合体(union),但功能更为强大。与联合体相比,std::variant具有类型安全性,可以判断当前存储的实际类型,并且可以存储结构体/类等复杂的数据结构。
preview 原理
我们依然采用“一图胜千言”的思想,给大家先展现下std::variant对应的UML图。这些图都是用我之前写的工具DotObject自动画出来的,有兴趣请参考《GDB调试技巧实战–自动化画出类关系图》,还有一篇应用实践《Modern C++利用工具快速理解std::tuple的实现原理》。
我们先举个简单的例子
std::variant<int,double>

解释一下:重点是**_Variadic_union**, 它是一个递归union,大概相当于c中的:
union _Variadic_union{数据 _M_first; //第N层的_Variadic_union _M_rest; //下一层还是一个union
}
另一个重点是:_Variant_storage::_M_index 是当前数据类型是可选类型列表中第几个,比如设置一个0.2,则当前类型是double, 此时_M_index=1(从0开始)。
std::variant的实现重点:存储
通过上面的preview,相信读者已经通过直观的认识快速入门并理解了底层是如何存储数据的了。
下面我们用GDB把数据打印出来看看:
variant<int,double> v1(3);

再赋值为2.0
v1 = 2.0;

在有了直观认识后,我们来看下源代码:
348 template<typename... _Types>349 union _Variadic_union { };350351 template<typename _First, typename... _Rest>352 union _Variadic_union<_First, _Rest...>353 {354 constexpr _Variadic_union() : _M_rest() { }355356 template<typename... _Args>357 constexpr _Variadic_union(in_place_index_t<0>, _Args&&... __args)358 : _M_first(in_place_index<0>, std::forward<_Args>(__args)...)359 { }360361 template<size_t _Np, typename... _Args>362 constexpr _Variadic_union(in_place_index_t<_Np>, _Args&&... __args)363 : _M_rest(in_place_index<_Np-1>, std::forward<_Args>(__args)...)364 { }365366 _Uninitialized<_First> _M_first;367 _Variadic_union<_Rest...> _M_rest;368 };
先不必管行356到364(问题一,这几行干啥用?),367行体现了递归的思想(递归在标准库实现中大量使用),每次都把第一个值单独拿出来。如果理解有困难,直接扔到cppinsights让它帮我们展开(为了方便cppinsights展开,我把_M_first先简化为_First类型了,之后再详细分析它):

可以看到_Variadic_union<int,double> = int _M_first + _Variadic_union _M_rest
_Variadic_union = double _M_first + _Variadic_union<>
OK, _M_rest是为了递归,哪_M_first哪?当然,大家已经看到它对应每层的数据,不过它的实际类型是**_Uninitialized**,在我们的例子中分别对应只包含int或double的结构体,

不要简单的以为总是“类型 _M_storage”, 看下不是int、double等简单类型而是一个结构体或类会怎么样?
class Person{public:Person(const string& name, int age):_name(name),_age(age){}~Person(){cout<<"Decons Person:";print();}void print(){cout<<"name="<<_name<<" age="<<_age<<endl;}private:string _name;int _age;
};
int main(){variant<int,Person> v2;
}

可见它把Person变成了char[40], 40恰为Person的size大小。
让我们看下**_Uninitialized**的定义:
227 // _Uninitialized<T> is guaranteed to be a trivially destructible type,228 // even if T is not.229 template<typename _Type, bool = std::is_trivially_destructible_v<_Type>>230 struct _Uninitialized;231232 template<typename _Type>233 struct _Uninitialized<_Type, true>234 {235 template<typename... _Args>236 constexpr237 _Uninitialized(in_place_index_t<0>, _Args&&... __args)238 : _M_storage(std::forward<_Args>(__args)...)239 { }240241 constexpr const _Type& _M_get() const & noexcept242 { return _M_storage; }243244 constexpr _Type& _M_get() & noexcept245 { return _M_storage; }246247 constexpr const _Type&& _M_get() const && noexcept248 { return std::move(_M_storage); }249250 constexpr _Type&& _M_get() && noexcept251 { return std::move(_M_storage); }252253 _Type _M_storage;254 };255256 template<typename _Type>257 struct _Uninitialized<_Type, false>258 {259 template<typename... _Args>260 constexpr261 _Uninitialized(in_place_index_t<0>, _Args&&... __args)262 {263 ::new ((void*)std::addressof(_M_storage))264 _Type(std::forward<_Args>(__args)...);265 }266267 const _Type& _M_get() const & noexcept268 { return *_M_storage._M_ptr(); }269270 _Type& _M_get() & noexcept271 { return *_M_storage._M_ptr(); }272273 const _Type&& _M_get() const && noexcept274 { return std::move(*_M_storage._M_ptr()); }275276 _Type&& _M_get() && noexcept277 { return std::move(*_M_storage._M_ptr()); }278279 __gnu_cxx::__aligned_membuf<_Type> _M_storage;280 };
很明显,针对is_trivially_destructible_v是true、false各有一个特化,type为Person时命中_Uninitialized<_Type, false>(因为它有自己定义的析构函数,故is_trivially_destructible_v==false, 细节请参考下面的截图)
std::variant的实现重点:get(获取值)
存是递归,取也是递归取
给出任意一个variant object, 比如v1, 我们知道
- 数据类型对应的下标是v1._M_index
- 数据存在v1._M_u
则要想获得第一个数据类型的值只需return v1._M_u._M_first
要想获得第二个数据类型的值只需return v1._M_u._M_rest._M_first
要想获得第三个数据类型的值只需return v1._M_u._M_rest._M_rest._M_first
… …
这正是源代码的实现方式:
282 template<typename _Union>283 constexpr decltype(auto) //获得第一个数据类型的值 我们的例子中是int284 __get(in_place_index_t<0>, _Union&& __u) noexcept285 { return std::forward<_Union>(__u)._M_first._M_get(); }286287 template<size_t _Np, typename _Union>288 constexpr decltype(auto) //获得第N个数据类型的值 我们的例子中第二个是double289 __get(in_place_index_t<_Np>, _Union&& __u) noexcept290 {291 return __variant::__get(in_place_index<_Np-1>, //递归292 std::forward<_Union>(__u)._M_rest);293 }294295 // Returns the typed storage for __v.296 template<size_t _Np, typename _Variant>297 constexpr decltype(auto)298 __get(_Variant&& __v) noexcept299 {300 return __variant::__get(std::in_place_index<_Np>,301 std::forward<_Variant>(__v)._M_u);302 }
对照上面的实现想一想下面的代码如何运行的?
variant<int,double> v(1.0);
cout<<get<1>(v);
这个哪?
std::variant<int, double, char, string> myVariant("mzhai");
string s = get<3>(myVariant);
需要很多次._M_rest对不? 所以如果你非常看重效率,那么请把常用的类型安排在前面,比如把上面的代码改成:
std::variant<string, int, double, char> myVariant("mzhai");
std::variant的实现重点:赋值
赋值大体有三种办法:
- 初始化(调用构造函数)
- 重新赋值 (调用operator = )
- 重新赋值 (调用emplace)
但赋值很复杂,因为情况很多: - variant alternatives都是int般简单类型,=右边也是简单类型
- variant alternatives都是trivial类,=右边也是trivial类
- variant alternatives都是非trivial类,=右边也是非trivial类
- variant alternatives都是非trivial类,=右边是构造非trivial类的参数
- variant alternatives都是非trivial类,而且有些类的ctor或mtor或assignment operator被删除
- copy assignment/ move assignment 会抛异常导致valueless
- …
情况多的不厌其烦。头大。
我们只挑最简单的说下(捏个软柿子~), 考虑如下代码:
std::variant<int, double> v;
v = 2.0f;
对应的实现为:
1456 template<typename _Tp>
1457 enable_if_t<__exactly_once<__accepted_type<_Tp&&>>
1458 && is_constructible_v<__accepted_type<_Tp&&>, _Tp>
1459 && is_assignable_v<__accepted_type<_Tp&&>&, _Tp>,
1460 variant&>
1461 operator=(_Tp&& __rhs)
1462 noexcept(is_nothrow_assignable_v<__accepted_type<_Tp&&>&, _Tp>
1463 && is_nothrow_constructible_v<__accepted_type<_Tp&&>, _Tp>)
1464 {
1465 constexpr auto __index = __accepted_index<_Tp>;
1466 if (index() == __index) //index()为0,因为初始化v时以int初始化,__index为1//这种情况对应的是前面那次赋值和这次赋值类型一样。比如v=1.0; v=2.0
1467 std::get<__index>(*this) = std::forward<_Tp>(__rhs);
1468 else
1469 {//前后两次赋值类型不一样,比如v=1; v=2.0. 本例v=2.0f走这里。
1470 using _Tj = __accepted_type<_Tp&&>;
1471 if constexpr (is_nothrow_constructible_v<_Tj, _Tp>
1472 || !is_nothrow_move_constructible_v<_Tj>)
1473 this->emplace<__index>(std::forward<_Tp>(__rhs));//本例走这
1474 else
1475 operator=(variant(std::forward<_Tp>(__rhs)));
1476 }
1477 return *this;
1478 }1499 template<size_t _Np, typename... _Args>
1500 enable_if_t<is_constructible_v<variant_alternative_t<_Np, variant>,
1501 _Args...>,
1502 variant_alternative_t<_Np, variant>&>
1503 emplace(_Args&&... __args)
1504 {
1505 static_assert(_Np < sizeof...(_Types),
1506 "The index must be in [0, number of alternatives)");
1507 using type = variant_alternative_t<_Np, variant>;
1508 namespace __variant = std::__detail::__variant;
1509 // Provide the strong exception-safety guarantee when possible,
1510 // to avoid becoming valueless.
1511 if constexpr (is_nothrow_constructible_v<type, _Args...>)
1512 {
1513 this->_M_reset(); //析构原对象,并置_M_index=-1
1514 __variant::__construct_by_index<_Np>(*this, //placement new,构造新值
1515 std::forward<_Args>(__args)...);
1516 }
1517 else if constexpr (is_scalar_v<type>)
1518 {
1519 // This might invoke a potentially-throwing conversion operator:
1520 const type __tmp(std::forward<_Args>(__args)...);
1521 // But these steps won't throw:
1522 this->_M_reset();
1523 __variant::__construct_by_index<_Np>(*this, __tmp);
1524 }
1525 else if constexpr (__variant::_Never_valueless_alt<type>()
1526 && _Traits::_S_move_assign)
析构原来的类型对象和构造新的类型对象请分别参考_M_reset __construct_by_index
422 void _M_reset()423 {424 if (!_M_valid()) [[unlikely]]425 return;426427 std::__do_visit<void>([](auto&& __this_mem) mutable428 {429 std::_Destroy(std::__addressof(__this_mem));430 }, __variant_cast<_Types...>(*this));431432 _M_index = static_cast<__index_type>(variant_npos);433 }1092 template<size_t _Np, typename _Variant, typename... _Args>
1093 inline void
1094 __construct_by_index(_Variant& __v, _Args&&... __args)
1095 {
1096 auto&& __storage = __detail::__variant::__get<_Np>(__v);
1097 ::new ((void*)std::addressof(__storage))
1098 remove_reference_t<decltype(__storage)>
1099 (std::forward<_Args>(__args)...);
1100 // Construction didn't throw, so can set the new index now:
1101 __v._M_index = _Np;
1102 }
赋值原理基本大体如此,如有读者感觉意犹未尽,这里我给一个程序供大家调试研究思考:
#include<iostream>
#include<variant>
using namespace std;int main(){class C1{public:C1(int i):_i(i){}private:int _i;};cout<<is_nothrow_constructible_v<C1><<endl;cout<<is_nothrow_move_constructible_v<C1><<endl;variant<string, C1> v;v = 10; //重点在这里return 0;
}
提示:
- 没走emplace, 走了1475 operator=(variant(std::forward<_Tp>(__rhs)));
- 还记得上面我们留了一个问题吗?
先不必管行356到364(问题一,这几行干啥用?)
本例调用了362的构造函数 constexpr _Variadic_union(in_place_index_t<_Np>, _Args&&… __args)
这个构造函数就是为类类型(有parameterized constructor)准备的。
相关文章:
Modern C++ std::variant的实现原理
前言 std::variant是C17标准库引入的一种类型,用于安全地存储和访问多种类型中的一种。它类似于C语言中的联合体(union),但功能更为强大。与联合体相比,std::variant具有类型安全性,可以判断当前存储的实际…...
⭐北邮复试刷题LCR 018. 验证回文串__双指针 (力扣119经典题变种挑战)
LCR 018. 验证回文串 给定一个字符串 s ,验证 s 是否是 回文串 ,只考虑字母和数字字符,可以忽略字母的大小写。 本题中,将空字符串定义为有效的 回文串 。 示例 1: 输入: s “A man, a plan, a canal: Panama” 输出: true 解释…...
C++面试:数据库的权限管理数据库的集群和高可用
目录 一、数据库的权限管理 1. 用户和角色管理 用户管理 实例举例(以MySQL为例): 角色管理 实例举例(以MySQL为例): 总结 2. 权限和授权 用户和角色管理 用户管理 角色管理 权限和授权 权限 授…...
个人搭建部署gpt站点
2024搭建部署gpt 参照博客 https://cloud.tencent.com/developer/article/2266669?areaSource102001.19&traceIdRmFvGjZ9BeaIaFEezqQBj博客核心点 准备好你的 OpenAI API Key; 点击右侧按钮开始部署: Deploy with Vercel,直接使用 Github 账号登…...
samber/lo 库的使用方法: condition
samber/lo 库的使用方法: condition samber/lo 是一个 Go 语言库,使用泛型实现了一些常用的操作函数,如 Filter、Map 和 FilterMap。汇总目录页面 这个库函数太多,因此我决定按照功能分别介绍,本文介绍的是 samber/l…...
Chrome插件精选 — 缓存清理
Chrome实现同一功能的插件往往有多款产品,逐一去安装试用耗时又费力,在此为某一类型插件挑选出比较好用的一款或几款,尽量满足界面精致、功能齐全、设置选项丰富的使用要求,便于节省一个个去尝试的时间和精力。 1. Chrome清理大师…...
Redis之缓存穿透问题解决方案实践SpringBoot3+Docker
文章目录 一、介绍二、方案介绍三、Redis Docker部署四、SpringBoot3 Base代码1. 依赖配置2. 基本代码 五、缓存优化代码1. 校验机制2. 布隆过滤器3. 逻辑优化 一、介绍 当一种请求,总是能越过缓存,调用数据库,就是缓存穿透。 比如当请求一…...
每日shell脚本之超级整合程序3.0
每日shell脚本之超级整合程序3.0 本期带来之前的升级版2.0整合脚本程序,学习工作小利器,同时模块化构建方便二次开发。 上图 上源码 #!/usr/bin/bash # *******************************************# # * CDDN : M乔木 # # * qq邮箱 …...
Docker介绍与使用
Docker介绍与使用 目录: 一、Docker介绍 1、Docker概述与安装 2、Docker三要素 二、Docker常用命令的使用 1、镜像相关命令 2、容器相关命令 三、Docker实战之下载mysql、redis、zimg 一、Docker介绍 Docker是一个开源的应用容器引擎,让开发者可以打包…...
Gin框架: 使用go-ini配置参数与不同环境下的配置部署
关于 INI 配置文件与go-ini 1 )概述 在INI配置文件中可以处理各种数据的配置INI文件是一种简单的文本格式,常用于配置软件的各种参数go-ini 是地表 最强大、最方便 和 最流行 的 Go 语言 INI 文件操作库 Github 地址:https://github.com/go-…...
探究网络工具nc(netcat)的使用方法及安装步骤
目录 🐶1. 什么是nc(netcat)? 🐶2. nc(netcat)的基本使用方法 2.1 🥙使用 nc 进行端口监听 2.2 🥙使用 nc 进行端口扫描 2.3 🥙使用 Netcat 进行文件传输…...
深入浅出JVM(四)之类文件结构
深入浅出JVM(四)之类文件结构 Java文件编译成字节码文件后,通过类加载机制到Java虚拟机中,Java虚拟机能够执行所有符合要求的字节码,因此无论什么语言,只要能够编译成符合要求的字节码文件就能够被Java虚拟…...
Anaconda下的pkgs占用空间13G,如何安全的清理(已解决)
方法一:让Anaconda自行决定清理 执行命令 conda clean -p 我的Anaconda安装在D盘,具体位置如下。你的应该也能找到对应的位置 D:\*****\**\Anaconda3\pkgs (base) C:\Users\Liu_J>conda clean -p WARNING: C:\Users\***\.conda\pkgs does not ex…...
压缩感知常用的重建算法
重建算法的基本概念 在压缩感知(Compressed Sensing, CS)框架中,重建算法是指将从原始信号中以低于奈奎斯特率采集得到的压缩测量值恢复成完整信号的数学和计算过程。由于信号在采集过程中被压缩,因此重建算法的目标是找到最符合…...
c语言经典测试题2
1.题1 我们来思考一下它的结果是什么? 我们来分析一下:\\是转义为字符\,\123表示的是一个八进制,算一个字符,\t算一个字符,加上\0,应该有13个,但是strlen只计算\0前的字符个数。所以…...
⭐北邮复试刷题105. 从前序与中序遍历序列构造二叉树__递归分治 (力扣每日一题)
105. 从前序与中序遍历序列构造二叉树 给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。 示例 1: 输入: preorder [3,9,20,15,7], inorder [9,3,15,…...
机房预约系统(个人学习笔记黑马学习)
1、机房预约系统需求 1.1系统简介 学校现有几个规格不同的机房,由于使用时经常出现“撞车“现象,现开发一套机房预约系统,解决这一问题。 1.2身份简介 分别有三种身份使用该程序 学生代表:申请使用机房教师:审核学生的预约申请管理员:给学生、教师创建账…...
7、内网安全-横向移动PTH哈希PTT票据PTK密匙Kerberos密码喷射
用途:个人学习笔记,有所借鉴,欢迎指正 目录 一、域横向移动-PTH-Mimikatz&NTLM 1、Mimikatz 2、impacket-at&ps&wmi&smb 二、域横向移动-PTK-Mimikatz&AES256 三、域横向移动-PTT-漏洞&Kekeo&Ticket 1、漏…...
【前端】夯实基础 css/html/js 50个练手项目(持续更新)
文章目录 前言Day 1 expanding-cardsDay 2 progress-steps 前言 发现一个没有用前端框架的练手项目,很适合我这种纯后端开发夯实基础,内含50个mini project,学习一下,做做笔记。 项目地址:https://github.com/bradtr…...
ELK入门(四)-logstash
Logstash Logstash 是开源的服务器端数据处理管道,能够同时从多个来源采集数据,转换数据,然后将数据发送到您最喜欢的存储库中。 Logstash 能够动态地采集、转换和传输数据,不受格式或复杂度的影响。利用 Grok 从非结构化数据中…...
第19节 Node.js Express 框架
Express 是一个为Node.js设计的web开发框架,它基于nodejs平台。 Express 简介 Express是一个简洁而灵活的node.js Web应用框架, 提供了一系列强大特性帮助你创建各种Web应用,和丰富的HTTP工具。 使用Express可以快速地搭建一个完整功能的网站。 Expre…...
【学习笔记】深入理解Java虚拟机学习笔记——第4章 虚拟机性能监控,故障处理工具
第2章 虚拟机性能监控,故障处理工具 4.1 概述 略 4.2 基础故障处理工具 4.2.1 jps:虚拟机进程状况工具 命令:jps [options] [hostid] 功能:本地虚拟机进程显示进程ID(与ps相同),可同时显示主类&#x…...
在web-view 加载的本地及远程HTML中调用uniapp的API及网页和vue页面是如何通讯的?
uni-app 中 Web-view 与 Vue 页面的通讯机制详解 一、Web-view 简介 Web-view 是 uni-app 提供的一个重要组件,用于在原生应用中加载 HTML 页面: 支持加载本地 HTML 文件支持加载远程 HTML 页面实现 Web 与原生的双向通讯可用于嵌入第三方网页或 H5 应…...
重启Eureka集群中的节点,对已经注册的服务有什么影响
先看答案,如果正确地操作,重启Eureka集群中的节点,对已经注册的服务影响非常小,甚至可以做到无感知。 但如果操作不当,可能会引发短暂的服务发现问题。 下面我们从Eureka的核心工作原理来详细分析这个问题。 Eureka的…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
PostgreSQL——环境搭建
一、Linux # 安装 PostgreSQL 15 仓库 sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-$(rpm -E %{rhel})-x86_64/pgdg-redhat-repo-latest.noarch.rpm# 安装之前先确认是否已经存在PostgreSQL rpm -qa | grep postgres# 如果存在࿰…...
淘宝扭蛋机小程序系统开发:打造互动性强的购物平台
淘宝扭蛋机小程序系统的开发,旨在打造一个互动性强的购物平台,让用户在购物的同时,能够享受到更多的乐趣和惊喜。 淘宝扭蛋机小程序系统拥有丰富的互动功能。用户可以通过虚拟摇杆操作扭蛋机,实现旋转、抽拉等动作,增…...
WebRTC从入门到实践 - 零基础教程
WebRTC从入门到实践 - 零基础教程 目录 WebRTC简介 基础概念 工作原理 开发环境搭建 基础实践 三个实战案例 常见问题解答 1. WebRTC简介 1.1 什么是WebRTC? WebRTC(Web Real-Time Communication)是一个支持网页浏览器进行实时语音…...
Modbus RTU与Modbus TCP详解指南
目录 1. Modbus协议基础 1.1 什么是Modbus? 1.2 Modbus协议历史 1.3 Modbus协议族 1.4 Modbus通信模型 🎭 主从架构 🔄 请求响应模式 2. Modbus RTU详解 2.1 RTU是什么? 2.2 RTU物理层 🔌 连接方式 ⚡ 通信参数 2.3 RTU数据帧格式 📦 帧结构详解 🔍…...
Python训练营-Day26-函数专题1:函数定义与参数
题目1:计算圆的面积 任务: 编写一个名为 calculate_circle_area 的函数,该函数接收圆的半径 radius 作为参数,并返回圆的面积。圆的面积 π * radius (可以使用 math.pi 作为 π 的值)要求:函数接收一个位置参数 radi…...
