Modern C++ std::variant的实现原理

2024-02-24 19:36

本文主要是介绍Modern C++ std::variant的实现原理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1. 前言

std::variant是C++17标准库引入的一种类型,用于安全地存储和访问多种类型中的一种。它类似于C语言中的联合体(union),但功能更为强大。与联合体相比,std::variant具有类型安全性,可以判断当前存储的实际类型,并且可以存储结构体/类等复杂的数据结构。

2. preview 原理

我们依然采用“一图胜千言”的思想,给大家先展现下std::variant对应的UML图。这些图都是用我之前写的工具DotObject自动画出来的,有兴趣请参考《GDB调试技巧实战–自动化画出类关系图》,还有一篇应用实践《Modern C++利用工具快速理解std::tuple的实现原理》。
我们先举个简单的例子, 看下variant的庐山真面目。

std::variant<int,double>

请添加图片描述

3. std::variant的实现重点:存储

通过上面的preview,相信读者已经通过直观的认识快速入门并理解了底层是如何存储数据的了。
解释一下
(1)重点是_Variadic_union, 它是一个递归union,大概相当于c中的:

union _Variadic_union{数据  _M_first; //第N层的_Variadic_union  _M_rest;  //下一层还是一个union
}

(2) 另一个重点是:_Variant_storage::_M_index 是当前数据类型是可选类型列表中第几个,比如设置一个0.2,则当前类型是double, 此时_M_index=1(从0开始)。

下面我们用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 哪?当然,大家已经看到它对应每层的数据(int/double),不过它的实际类型是 _Uninitialized,在我们的例子中分别对应只包含int或double的结构体,
在这里插入图片描述
不要简单的以为_Uninitialized总是“类型 _M_storage”, 看下存结构体或类会怎么样?

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, 细节请参考下面的截图)

这里是引用

4. std::variant的实现重点:get(获取值)

存是递归,取也是递归取
给出任意一个variant object, 比如v1, 我们知道

  1. 数据类型对应的下标是v1._M_index
  2. 数据存在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");

后来注释,这是debug的情况,开启优化后没有性能问题。 请参考 《Modern C++ std::variant 我小看了get的速度》。

5. std::variant的实现重点:赋值

赋值大体有三种办法:

  1. 初始化(调用构造函数)
  2. 重新赋值 (调用operator = )
  3. 重新赋值 (调用emplace)

但赋值很复杂,因为情况很多:

  1. variant alternatives都是int般简单类型,=右边也是简单类型
  2. variant alternatives都是trivial类,=右边也是trivial类
  3. variant alternatives都是非trivial类,=右边也是非trivial类
  4. variant alternatives都是非trivial类,=右边是构造非trivial类的参数
  5. variant alternatives都是非trivial类,而且有些类的ctor或mtor或assignment operator被删除
  6. copy assignment/ move assignment 会抛异常导致valueless

  7. 情况多的不厌其烦。头大。
    我们只挑最简单的说下(捏个软柿子~), 考虑如下代码:
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;
}

提示:

  1. 没走emplace, 走了1475 operator=(variant(std::forward<_Tp>(__rhs)));
  2. 还记得上面我们留了一个问题吗?

先不必管行356到364(问题一,这几行干啥用?

本例调用了362的构造函数 constexpr _Variadic_union(in_place_index_t<_Np>, _Args&&… __args)
这个构造函数就是为类类型(有parameterized constructor)准备的。

这篇关于Modern C++ std::variant的实现原理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/743142

相关文章

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

hdu4407(容斥原理)

题意:给一串数字1,2,......n,两个操作:1、修改第k个数字,2、查询区间[l,r]中与n互质的数之和。 解题思路:咱一看,像线段树,但是如果用线段树做,那么每个区间一定要记录所有的素因子,这样会超内存。然后我就做不来了。后来看了题解,原来是用容斥原理来做的。还记得这道题目吗?求区间[1,r]中与p互质的数的个数,如果不会的话就先去做那题吧。现在这题是求区间[l,r]中与n互质的数的和

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo