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 从非结构化数据中…...
重新定义交通安全研究范式:基于无人机轨迹数据的数字孪生解决方案
重新定义交通安全研究范式:基于无人机轨迹数据的数字孪生解决方案 【免费下载链接】UCF-SST-CitySim1-Dataset 项目地址: https://gitcode.com/gh_mirrors/ucf/UCF-SST-CitySim-Dataset 在自动驾驶技术快速发展的今天,传统交通安全研究面临着一个…...
UDOP-large实战手册:英文技术文档FAQ自动生成Prompt模板库
UDOP-large实战手册:英文技术文档FAQ自动生成Prompt模板库 1. 引言:当技术文档遇上智能问答 想象一下这个场景:你刚拿到一份50页的英文技术白皮书,需要快速了解它的核心内容。传统做法是什么?打开PDF,从头…...
Krita AI Diffusion插件IP-Adapter缺失问题深度解析与实战解决方案
Krita AI Diffusion插件IP-Adapter缺失问题深度解析与实战解决方案 【免费下载链接】krita-ai-diffusion Streamlined interface for generating images with AI in Krita. Inpaint and outpaint with optional text prompt, no tweaking required. 项目地址: https://gitcod…...
Voron 2.4 3D打印机进阶调试与故障排除指南
Voron 2.4 3D打印机进阶调试与故障排除指南 【免费下载链接】Voron-2 Voron 2 CoreXY 3D Printer design 项目地址: https://gitcode.com/gh_mirrors/vo/Voron-2 机械系统精调:从结构应力到运动精度 问题导向:框架组装后出现对角线偏差超过2mm&a…...
C语言浪漫玫瑰代码:用编程传递爱意的创意实践
1. 用代码绽放爱的玫瑰:程序员专属浪漫指南 当传统玫瑰花束遇上代码,会碰撞出怎样的火花?作为一名写过无数行代码的老程序员,我发现用C语言绘制玫瑰花不仅能展现技术实力,更能传递独特的情感温度。记得第一次给女友展…...
大数据领域 OLAP 技术的发展趋势展望
大数据领域OLAP技术的发展趋势展望 关键词:OLAP、大数据分析、实时决策、云原生、AI融合 摘要:本文从超市老板的"销售密码"故事出发,用通俗易懂的语言拆解OLAP(在线分析处理)技术的核心逻辑,结合当前大数据技术演进趋势,深入探讨OLAP在实时化、云原生化、AI融…...
Fish Speech 1.5 Web界面保姆级教程:上传参考音频→文本对齐→语音生成全链路
Fish Speech 1.5 Web界面保姆级教程:上传参考音频→文本对齐→语音生成全链路 你是不是也想用AI生成和自己声音一模一样的语音?Fish Speech 1.5就能帮你实现这个愿望!这个强大的语音合成工具不仅能生成自然流畅的语音,还能通过参…...
Phi-3-mini-4k-instruct-gguf效果实测:128ms首token延迟+98%中文基础任务通过率
Phi-3-mini-4k-instruct-gguf效果实测:128ms首token延迟98%中文基础任务通过率 1. 开篇:轻量级文本生成新选择 最近测试了微软Phi-3系列中的轻量级选手——Phi-3-mini-4k-instruct-gguf模型,结果让人惊喜。这个专门优化过的GGUF版本&#x…...
PyTorch 2.8镜像法律科技:庭审音视频→AI摘要+关键帧提取+证据链可视化
PyTorch 2.8镜像法律科技:庭审音视频→AI摘要关键帧提取证据链可视化 1. 镜像概述与法律科技应用场景 在司法数字化进程中,庭审音视频处理正面临三大挑战:海量数据难以快速消化、关键信息提取效率低下、证据呈现方式单一。PyTorch 2.8深度学…...
Python内存管理策略对比评测报告(2024权威版):仅1种策略通过了金融级SLA压力测试,其余4种已淘汰
第一章:Python智能体内存管理策略对比评测报告(2024权威版)概述Python智能体(如基于LLM的Agent框架、自主任务调度器、多步推理引擎)在运行过程中面临高频对象创建、长生命周期缓存、跨线程引用共享等复杂内存场景。传…...
