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

1. 前言

std::any 是 C++17 中引入的一个新特性,它是一个类型安全的容器,可以在其中存储任何类型(但此类型必须可拷贝构造)的值,包括基本类型、自定义类型、指针等。相比于void* 指针,std::any 更为类型安全,可以避免由于类型转换错误而导致的运行时错误。

std::any 获取值时需要指定正确的类型。如果尝试获取的类型与存储的类型不匹配,将抛出 std::bad_any_cast 异常。

2. std::any初体验

cppreference上就有一个例子,copy过来供大家学习:

#include <any>
#include <iostream>int main()
{std::cout << std::boolalpha;// any typestd::any a = 1;std::cout << a.type().name() << ": " << std::any_cast<int>(a) << '\n';a = 3.14;std::cout << a.type().name() << ": " << std::any_cast<double>(a) << '\n';a = true;std::cout << a.type().name() << ": " << std::any_cast<bool>(a) << '\n';// bad casttry{a = 1;std::cout << std::any_cast<float>(a) << '\n';}catch (const std::bad_any_cast& e){std::cout << e.what() << '\n';}// has valuea = 2;if (a.has_value())std::cout << a.type().name() << ": " << std::any_cast<int>(a) << '\n';// reseta.reset();if (!a.has_value())std::cout << "no value\n";// pointer to contained dataa = 3;int* i = std::any_cast<int>(&a);std::cout << *i << '\n';
}

输出:

int: 1
double: 3.14
bool: true
bad any_cast
int: 2
no value
3 

正如介绍中所说:

std::any可以有值,可以无值,还能存储int/double/bool等等类型数据,获取值时如果类型不对会抛出异常。

3. 原理-存储

any的存储是所有container中最简单的一个了,全部实现才600来行,通读一点问题没有。

any就是一个普通的类,没有任何模板参数,其 实现 在文件/usr/include/c++/11/any中。

class std::any {private:void (*_M_manager)(std::any::_Op, const std::any *, std::any::_Arg *);std::any::_Storage _M_storage;
}union std::any::_Storage {void *_M_ptr;std::aligned_storage<8, 8>::type _M_buffer; //size与指针相同,要求8字节对齐
}

std::any只有两项数据成员:

  1. _M_storage:数据存在这。因为用到了小对象优化,所以是一个union,笼统的讲:小对象用栈空间_M_buffer, 大点的对象要在堆上分配空间,由_M_ptr指向,不过这么讲不是很准确。
  2. _M_manager:一个特殊的函数,用来操作_M_storage,操作类型包括:访问、克隆(针对any copy)、移动(针对move语义)等等。

本节着重于第一条_M_storage,第二条我们在后面的“原理-初始化” 和 “原理-获取数据 ”中会详细叙述。 

3.1 存储-直观认识

关于_M_storage的内容,我们先看两个实例,从例子中有个直观认识:

std::any a = 1;

_M_ptr=1肯定不合法,1是按字节存入了char _M_buffer[8], 我的机器因为是小端故第一个字节是\001. 所以这里用了小对象优化。

再给a赋值为一个字符串,看看8个字节存不下的情况:

a = string("mzhai");

 可以看到new出了新空间,在堆上存储了string “mzhai”,由_M_ptr指向它。

3.2 存储-源码

OK,让我们看看源码,它是如何判断用不用小对象优化的哪?

 94     template<typename _Tp, typename _Safe = is_nothrow_move_constructible<_Tp>,95          bool _Fits = (sizeof(_Tp) <= sizeof(_Storage))96               && (alignof(_Tp) <= alignof(_Storage))>97       using _Internal = std::integral_constant<bool, _Safe::value && _Fits>;

_Internal见名知意,用“内部”栈空间的意思,其值由两点确定:

  1. _Tp必须是可移动构造 且 不抛异常的。
  2. _Tp的size必须小于预留的栈空间大小(8字节),且对齐要求小于等于8。

4. 原理-初始化

现在我们已经知道了any有两个重要的成员变量 以及 数据存在哪。我们想进一步看看如何由别的数据类型初始化一个any对象哪?这两个成员变量都是如何赋的值哪?

除了默认构造函数还有四种方式初始化一个any对象:

any(_Tp&& __value) //调用_Tp copy ctor/move ctor
any(in_place_type_t<_Tp>, _Args&&... __args)  //调用_Tp parameterized ctor
any(in_place_type_t<_Tp>, initializer_list<_Up> __il, _Args&&... __args) //调用_Tp parameterized ctoroperator=(_Tp&& __rhs)

如果你不太熟悉前三种用法,请参考官方链接。

一旦初始化了一个any对象,就可以用它的本身的copy ctor/move ctor来初始化别的any对象,所以上面这四个是基础。

前三个很相似,我是指它们的流程,流程分三步:

1. 它们都有激活的条件至少要求Tp是可拷贝构造的

比如第一个要求:is_copy_constructible<_VTp>

后面的要求更多,不仅仅要求is_copy_constructible,在此不再展开介绍。

2. 初始化_M_manager它是一个函数指针,要么指向小对象处理函数_Manager_internal::_S_manage,要么指向非小对象处理函数_Manager_external::_S_manage。

    template <typename _Tp, typename _VTp = _Decay_if_not_any<_Tp>,typename _Mgr = _Manager<_VTp>,typename = _Require<__not_<__is_in_place_type<_VTp>>,is_copy_constructible<_VTp>>>any(_Tp&& __value): _M_manager(&_Mgr::_S_manage)template<typename _Tp, typename _Safe = is_nothrow_move_constructible<_Tp>,bool _Fits = (sizeof(_Tp) <= sizeof(_Storage))&& (alignof(_Tp) <= alignof(_Storage))>using _Internal = std::integral_constant<bool, _Safe::value && _Fits>;template<typename _Tp>struct _Manager_internal; // uses small-object optimizationtemplate<typename _Tp>struct _Manager_external; // creates contained object on the heap//根据前面一节"原理-存储"中介绍的判断来决定用不用小对象优化template<typename _Tp>using _Manager = conditional_t<_Internal<_Tp>::value,_Manager_internal<_Tp>,_Manager_external<_Tp>>;

无论_Manager_internal还是_Manager_external 都定义了四个相同的静态函数:

  1.  _S_manage(_Op __which, const any* __anyp, _Arg* __arg); 
  2. _S_create(_Storage& __storage, _Up&& __value)       //下面的“存入数据”会用到
  3. _S_create(_Storage& __storage, _Args&&... __args)  //下面的“存入数据”会用到
  4. _S_access(const _Storage& __storage)                      //any_cast会用到

我们的关注点在_S_manage,简单比较下_Manager_internal::_S_manage 与 _Manager_external::_S_manage的不同:

template<typename _Tp>    any::_Manager_internal<_Tp>::_S_manage(_Op __which, const any* __any, _Arg* __arg){// 从char _M_storage._M_buffer[8]中取数据auto __ptr = reinterpret_cast<const _Tp*>(&__any->_M_storage._M_buffer);switch (__which){case _Op_access:__arg->_M_obj = const_cast<_Tp*>(__ptr);break;template<typename _Tp>voidany::_Manager_external<_Tp>::_S_manage(_Op __which, const any* __any, _Arg* __arg){// 从_M_storage._M_ptr指向的内存中取数据auto __ptr = static_cast<const _Tp*>(__any->_M_storage._M_ptr);switch (__which){case _Op_access:__arg->_M_obj = const_cast<_Tp*>(__ptr);break;

3. 存入数据

any中的两大数据成员之一_M_manager已经搞定,下一步就是_M_storage。

_M_storage的初始化,无论是三种初始化中的哪种,都是委托给_Mgr::_S_create完成的。比如第一种:

      any(_Tp&& __value): _M_manager(&_Mgr::_S_manage){_Mgr::_S_create(_M_storage, std::forward<_Tp>(__value));}

当然根据是否应用了SSO, _S_create也分两种情况:

1. _Manager_internal 调用了placement new在原有内存上构造Tp:

template<typename _Up>static void_S_create(_Storage& __storage, _Up&& __value){void* __addr = &__storage._M_buffer;::new (__addr) _Tp(std::forward<_Up>(__value)); //调用copy ctor/move ctor}template<typename... _Args>static void_S_create(_Storage& __storage, _Args&&... __args){void* __addr = &__storage._M_buffer;::new (__addr) _Tp(std::forward<_Args>(__args)...); //调用parameterized ctor}

 2. _Manager_external在堆上构造Tp

	template<typename _Up>static void_S_create(_Storage& __storage, _Up&& __value){__storage._M_ptr = new _Tp(std::forward<_Up>(__value));}template<typename... _Args>static void_S_create(_Storage& __storage, _Args&&... __args){__storage._M_ptr = new _Tp(std::forward<_Args>(__args)...);}

OK,前三种初始化讲完了,来看下第四种:assignement operator 

   template<typename _Tp>enable_if_t<is_copy_constructible<_Decay_if_not_any<_Tp>>::value, any&>operator=(_Tp&& __rhs){*this = any(std::forward<_Tp>(__rhs));return *this;}

它先调用了第一种初始化方式构造了一个临时any对象,再调用any的mtor把数据move过去,move委托给了_Manager_**ternal::_S_manage

    any(any&& __other) noexcept{if (!__other.has_value())_M_manager = nullptr;else{_Arg __arg;__arg._M_any = this;__other._M_manager(_Op_xfer, &__other, &__arg);}}

5. 原理-获取数据 

数据的获取必须通过any_cast,其语法如下:

无论是哪种形式,都会调用到__any_caster:

  template<typename _Tp>void* __any_caster(const any* __any){// any_cast<T> returns non-null if __any->type() == typeid(T) and// typeid(T) ignores cv-qualifiers so remove them:using _Up = remove_cv_t<_Tp>;// The contained value has a decayed type, so if decay_t<U> is not U,// then it's not possible to have a contained value of type U:if constexpr (!is_same_v<decay_t<_Up>, _Up>)return nullptr;// 又一次要求存储的类型必须拷贝可构造!else if constexpr (!is_copy_constructible_v<_Up>)return nullptr;// First try comparing function addresses, which works without RTTIelse if (__any->_M_manager == &any::_Manager<_Up>::_S_manage
#if __cpp_rtti|| __any->type() == typeid(_Tp)
#endif){return any::_Manager<_Up>::_S_access(__any->_M_storage); //真正的访问数据}return nullptr;}

 当然根据是否用了SSO,依然有两种情况。代码比较简单,我就不解释了。

//_Manager_internal	 SSOstatic _Tp*_S_access(const _Storage& __storage){// The contained object is in __storage._M_bufferconst void* __addr = &__storage._M_buffer;return static_cast<_Tp*>(const_cast<void*>(__addr));}//_Manager_externalstatic _Tp*_S_access(const _Storage& __storage){// The contained object is in *__storage._M_ptrreturn static_cast<_Tp*>(__storage._M_ptr);}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/707545.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

NC65 rest接口 开发 NC65接口开发

一、在对应模块META-INF下编写 xxx.rest 文件,也要放在Home里对应的目录下。 二、开发接口&#xff0c;继承extends AbstractUAPRestResource&#xff0c;&#xff08;有的项目会继承别的方法如&#xff1a;AbstractNCCRestResource&#xff0c;MTFRestResource&#xff1b;有…

网络安全面试题

1. Http 状态码&#xff0c;Http2 是什么 答案&#xff1a; 200 欢迎回来&#xff0c;主人 &#xff08;正常&#xff1b;请求已完成。&#xff09; 301 人家搬家了 &#xff08;已移动 — 请求的数据具有新的位置且更改是永久的。&#xff09; 307 不是这里&#xff0c;换个…

使用 kubeadm 部署k8s集群

一、所有节点系统初始化 1、常规初始化 2、内核版本升级以及内核限制文件参数修改 还可以考虑将旧版本的内核卸载 二、准备nginx负载均衡器和keepalived nginx四层代理&#xff1a; keepalived配置&#xff1a; nginx检测脚本&#xff1a; 三、所有节点部署docker&#xff0c…

SQL函数学习记录

聚合函数 函数是编程语言的基础之一&#xff0c;在对数字的运算中&#xff0c;我们用的最多的就是聚合函数&#xff0c;本篇接下来就详细阐述下SQL中聚合函数的运用。 什么是聚合函数&#xff08;aggregate function&#xff09;&#xff1f; 聚合函数指的是对一组值执行计算…

2023秋季飞书未来无限大会--随笔

这个时代的飞书 数字时代 工作协同平台 AI时代 帮助企业和个人用好AI 企业如何引用大模型能力&#xff1f; 智慧体— 接近人&#xff0c;有进步空间智能伙伴 用时代的科技打造爱不释手的好产品 移动互联网 – 改变信息分发方式 大模型 –自然的人机交互方式 业务协同 …

如何使用便签快速分类工作待办事项

在日常工作和生活中&#xff0c;我们经常需要处理各种各样的待办事项。而有效地分类这些任务&#xff0c;可以帮助我们更好地管理时间和提高工作效率。使用便签是一种简单而实用的方法&#xff0c;下面将介绍如何利用好用便签来快速分类工作待办事项。 首先&#xff0c;你可以…

【数据结构和算法初阶(C语言)】链表-单链表(手撕详讲单链表增删查改)

目录 1.前言&#xff1a;顺序表回顾&#xff1a; 1.1顺序表的优缺点 2.主角----链表 2.1链表的概念 2.2定义一个单链表的具体实现代码方式 3.单链表对数据的管理----增删查改 3.1单链表的创建 3.2单链表的遍历实现 3.2.1利用遍历实现一个打印我们链表内容的函数的函数…

【前端素材】推荐优质后台管理系统Salreo平台模板(附源码)

一、需求分析 当我们从多个层次来详细分析后台管理系统时&#xff0c;可以将其功能和定义进一步细分&#xff0c;以便更好地理解其在不同方面的作用和实际运作。 1. 结构层次 在结构层次上&#xff0c;后台管理系统可以分为以下几个部分&#xff1a; a. 辅助功能模块&#…

Mycat核心教程--ZooKeeper集群搭建【三】

Mycat核心教程--ZooKeeper集群搭建 八、 ZooKeeper集群搭建8.1.ZooKeeper简介8.2.数据复制的好处8.3.Zookeeper设计目的8.4.zookeeper集群包括3种角色8.4.1.Leader角色8.4.2.Follower 角色8.4.3.Observer 角色 8.5.zookeeper集群工作流程8.6.zookeeper集群节点数量为奇数&#…

JS进阶——深入对象

版权声明 本文章来源于B站上的某马课程&#xff0c;由本人整理&#xff0c;仅供学习交流使用。如涉及侵权问题&#xff0c;请立即与本人联系&#xff0c;本人将积极配合删除相关内容。感谢理解和支持&#xff0c;本人致力于维护原创作品的权益&#xff0c;共同营造一个尊重知识…

FPS游戏之漫谈延迟补偿技术

在FPS游戏中&#xff0c;延迟补偿是一种常用的技术&#xff0c;用于解决由于网络延迟导致的玩家体验不一致的问题。对于投掷手雷这样的动作&#xff0c;延迟补偿的具体策略可能包括以下几个方面&#xff1a; 时间回溯&#xff08;Lag Compensation&#xff09;&#xff1a;服务…

liunx操作系统 进程的基本概念

进程的基本概念 计算机结构体系冯诺依曼 操作系统的管理进程进程的特性标识符系统的调用 创建新的进程 进程的状态进程队列进程的状态在liunx查看进程状态、 计算机结构体系 冯诺依曼 在没有存储器之前&#xff0c;所有的信息都是直接进入CPU&#xff0c;这样效率很差&#xf…

智慧公厕:打造智慧城市环境卫生新标杆

随着科技的不断发展和城市化进程的加速推进&#xff0c;智慧城市建设已经成为各地政府和企业关注的焦点。而作为智慧城市环境卫生管理的基础设施&#xff0c;智慧公厕的建设和发展也备受重视&#xff0c;被誉为智慧城市的新标杆。本文以智慧公厕源头厂家广州中期科技有限公司&a…

【Zotero插件】zotero better notes与zotcard联合使用 | 学习资源整理

整体学习&#xff1a; zotero官网&#xff1a;Zotero | Your personal research assistant 官网插件入口&#xff1a;plugins [Zotero Documentation] 初步学习zotero&#xff1a; 【Zotero零基础保姆级教程】 https://www.bilibili.com/video/BV1o3411Q7JQ/?share_source…

Java代码实现获取本机服务的IP地址

要通过Java代码获取一个IP地址的信息&#xff0c;你可以使用多种方法。其中一种常见的做法是通过IP地址查询服务API来获取详细信息&#xff0c;比如地理位置、ISP&#xff08;互联网服务提供商&#xff09;等。这里有一个简单的例子&#xff0c;展示如何使用Java代码调用一个公…

RRT算法学习及MATLAB演示

文章目录 1 前言2 算法简介3 MATLAB实现3.1 定义地图3.2 绘制地图3.3 定义参数3.4 绘制起点和终点3.5 RRT算法3.5.1 代码3.5.2 效果3.5.3 代码解读 4 参考5 完整代码 1 前言 RRT&#xff08;Rapid Random Tree&#xff09;算法&#xff0c;即快速随机树算法&#xff0c;是LaVa…

Latex中大括号书写多行方式【已解决】

在写论文时需要写一个非1即0的公式&#xff0c;因此写了这篇文章。 本文主要分为两个部分&#xff1a;1.在括号外赋值、2、在括号内赋值 1.在括号外赋值 示例 \begin{equation}A\begin{cases}1x, x \textgreater 0\\1-x, x \leq 0 \end{cases} \label{KD} \end{equation} 效…

qt_xml文件

文章内容 简单介绍xml文件的增删改查写生成和读取xml文件的例子增删改查 Qt提供了QDomDocument类来操作XML文件。 增加节点: QDomElement root = doc.createElement("root"); doc.appendChild(root);QDomElement element = doc.createElement("element"…

Dockerfile(4) - RUN 指令详解

RUN 运行命令 shell 形式 命令在 shell 中运行Linux 上默认为 /bin/sh -cWindows 上 cmd /S /C RUN <command> exec 形式 RUN ["executable", "param1", "param2"] 必须双引号&#xff0c;不能是单引号 两种写法的实际栗子 RUN …

Linux系统运维命令:终止监听在 TCP端口80上的所有进程(使用lsof,grep,awk组合命令, 终止监听在 TCP某个端口上的所有进程)

目 录 一、需求 二、解决方法 1、解决思路 2、命令 三、实例演示和命令解释 1、实例演示 &#xff08;1&#xff09;查看目前有哪些在TCP端口80监听的进程 &#xff08;2&#xff09;、使用命令 &#xff08;3&#xff09;、查看效果 2、命令解…