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

news2024/11/19 14:34:13

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 type
    std::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 cast
    try
    {
        a = 1;
        std::cout << std::any_cast<float>(a) << '\n';
    }
    catch (const std::bad_any_cast& e)
    {
        std::cout << e.what() << '\n';
    }
 
    // has value
    a = 2;
    if (a.has_value())
        std::cout << a.type().name() << ": " << std::any_cast<int>(a) << '\n';
 
    // reset
    a.reset();
    if (!a.has_value())
        std::cout << "no value\n";
 
    // pointer to contained data
    a = 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 ctor

operator=(_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 optimization

    template<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>
    void
    any::_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 RTTI
      else 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	 SSO
    static _Tp*
	_S_access(const _Storage& __storage)
	{
	  // The contained object is in __storage._M_buffer
	  const void* __addr = &__storage._M_buffer;
	  return static_cast<_Tp*>(const_cast<void*>(__addr));
	}

//_Manager_external
	static _Tp*
	_S_access(const _Storage& __storage)
	{
	  // The contained object is in *__storage._M_ptr
	  return static_cast<_Tp*>(__storage._M_ptr);
	}

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

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

相关文章

级联选择el-cascader 动态加载后台数据,出现箭头和需要双击才能选中的问题

最近做项目遇到一个问题,就是用饿了么UI的级联选择器el-cascader 的时候,第三级出现了箭头,而且需要连续点击两次第三级才可以选中第三级的项,如下图: 由于数据量较大,我们用了懒加载,就是用户选中了第一级后,我会拿第一级的id,也就是第二级的父id去发请求,获取到第二…

【C语言】数据存储篇,内存中的数据存储----C语言整型,浮点数的数据在内存中的存储以及大小端字节序【图文详解】

欢迎来CILMY23的博客喔&#xff0c;本篇为​【C语言】数据存储篇&#xff0c;内存中的数据存储----C语言整型&#xff0c;浮点数的数据在内存中的存储以及大小端字节序【图文详解】&#xff0c;感谢观看&#xff0c;支持的可以给个一键三连&#xff0c;点赞关注收藏。 前言 C语…

Android Activity启动模式

文章目录 Android Activity启动模式概述四种启动模式Intent标记二者区别 Android Activity启动模式 概述 Activity 的管理方式是任务栈。栈是先进后出的结构。 四种启动模式 启动模式说明适用场景standard标准模式默认模式&#xff0c;每次启动Activity都会创建一个新的Act…

Qt CMake 国际化相关配置

文章目录 本来用qmake使用pro文件很简单的一件事&#xff0c;结果用cmake折腾了半天。 何必呢~ 参考&#xff1a;QT6.3 CMake 多语言切换 这是我的 cmake_minimum_required(VERSION 3.16)project(testQml3_6 VERSION 0.1 LANGUAGES CXX)set(CMAKE_AUTOMOC ON) set(CMAKE_CXX…

MySQL 篇-深入了解 DML、DQL 语言(二)

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 DML、DQL 语言说明 2.0 使用 DML 实现对数据管理和操作 2.1 DML - 增添数据 insert 2.2 DML - 修改数据 update 2.3 DML - 删除数据 delete 3.0 使用 DQL 实现对…

docker 容器修改端口和目录映射

一、容器修改端口映射 一般在运行容器时&#xff0c;我们都会通过参数 -p&#xff08;使用大写的-P参数则会随机选择宿主机的一个端口进行映射&#xff09;来指定宿主机和容器端口的映射&#xff0c;例如 docker run -it -d --name [container-name] -p 8088:80 [image-name]…

【EFK】基于K8S构建EFK+logstash+kafka日志平台

基于K8S构建EFKlogstashkafka日志平台 一、常见日志收集方案1.1、EFK1.2、ELK Stack1.3、ELK filbeat1.4、其他方案 二、EFK组件介绍2.1、Elasticsearch组件2.2、Filebeat组件【1】 Filebeat和beat关系【2】Filebeat是什么【3】Filebeat工作原理【4】传输方案 2.3、Logstash组件…

麒麟银河操作系统V10部署ffmpeg

麒麟银河操作系统V10部署ffmpeg 部署ffmpeg用来处理视频的各种操作 想使用ffmpeg&#xff0c;要先安装nasm&#xff0c;yasm&#xff0c;x264之后&#xff0c;否则会报错 nkvers 查看麒麟操作系统版本 cat /proc/version #查看linux版本信息 uname -a #查看linux版本和内核…

高分影像RPC校正工具使用步骤

高分影像准备 注意事项&#xff1a; 直接使用解压包&#xff0c;外部高分文件夹也要留存 文件夹内部不要有动作&#xff0c;就解压后的原始这些文件 最好不要有中文路径 xml中参数路径 入参包含三个&#xff0c;影像TIFF、影像RPB&#xff08;解压后自带&#xff09;、DEM&am…

电容元件及其特性

电容元件 电容器&#xff1a;由两块金属导体极板&#xff0c;中间间隔以不同的物质(如云母、绝缘纸、空气等)组成。 当在两极板之间加上电压后&#xff0c;两极板上分别聚集等量的正、负电荷&#xff0c;并在介质中建立电场且具有电场能量。 将电源移开后&#xff0c;电荷继续…

Java面向对象之接口和抽象类的区别一目了然

介绍 相信对于Java面向对象部分&#xff0c;很多人很长一段时间对于接口和抽象类的区别&#xff0c;使用场景都不是很熟悉&#xff0c;同是作为抽象层重要的对象&#xff0c;工作中到底什么情况下使用抽象类&#xff0c;不是很清楚。本文就一次性把这些概念一次性说清楚&#x…

Linux下性能分析的可视化图表工具

1 sar 和sadf 1.1 简介 sar命令可以记录系统下的常见活动信息&#xff0c;例如CPU使用率、网络统计数据、Block I/O数据、内存使用情况 等。 sar命令的“-o [file_name]”参数可以将系统活动数据记录到file_name文件&#xff0c;然后通过sadf来解析&#xff0c;sadf命令的“-g…

欢迎参与2024年 OpenTiny 开源项目用户调研

&#x1f389; 欢迎参与 OpenTiny 开源项目的用户调研 &#x1f389; &#x1f4e3; 调研背景 随着 OpenTiny 开源项目的不断发展&#xff0c;我们一直致力于为开发者提供高质量的 Web 前端开发解决方案。为了更好地满足用户需求&#xff0c;提升项目的实用性和易用性&#x…

CV及LLM常见名词解释

CV及LLM常见名词解释 看论文的时候&#xff0c;有些术语虽然常见但是却让人很难理解具体含义&#xff0c;如noise等&#xff0c;这里是一些常见术语的解释&#xff0c;同时也给出了这些术语所在的上下文及模型 例1&#xff1a;Stable Diffusion的文生图模块 Embedding&#…

IDEA开发环境热部署

开发环境热部署 在实际的项目开发调试过程中会频繁地修改后台类文件&#xff0c;导致需要重新编译重新启动&#xff0c;整个过程非常麻烦&#xff0c;影响开发效率。Spring Boot提供了spring-boot-devtools组件&#xff0c;使得无须手动重启SpringBoot应用即可重新编译、启动项…

亿道信息新品EM-T195轻薄型工业平板,隆重登场!

EM-T195是一款轻巧但坚固的平板电脑&#xff0c;仅 650克重、10.5mm毫米厚&#xff0c;即使没有额外的便携配件进行辅助&#xff0c;您也可以轻松将其长时间随身携带。耐用性外壳完全密封&#xff0c;防尘防潮&#xff1b;出色的坚固性和可靠性&#xff0c;使T195天生适合在苛刻…

Delegate(P29 5.5delegate)

一、Delegate简介 每个代理都可以访问许多附加的属性&#xff0c;其中一些来自数据模型&#xff0c;另一些来自视图。 从模型中&#xff08;Model&#xff09;&#xff1a;属性将每个项目的数据传递给 delegate。 从视图中&#xff08;View&#xff09;&#xff1a;属性将状…

Unity(第九部)物体类

拿到物体的某些数据 using System.Collections; using System.Collections.Generic; using UnityEngine;public class game : MonoBehaviour {// Start is called before the first frame updatevoid Start(){//拿到当前脚本所挂载的游戏物体//GameObject go this.gameObject;…

golang使用gorm操作mysql1

1.mysql连接配置 package daoimport ("fmt""gorm.io/driver/mysql""gorm.io/gorm""gorm.io/gorm/logger" )var DB *gorm.DB// 连接数据库&#xff0c;启动服务的时候&#xff0c;init方法就会执行 func init() {username : "roo…

Sentinel实战(待完善)

目录 服务雪崩 什么是服务雪崩 服务不可用原因 解决方案 技术选型对比 Sentinel 介绍 优点 核心概念 资源 规则 代码实战 API实现 SentinelResource注解埋点实现 服务雪崩 什么是服务雪崩 在服务调用链路中, 服务提供者不可用, 导致服务调用者不可用, 间接让上上游…