C++类型参数技术以及常见的类型擦除容器
文章目录
- 一、类型擦除的作用
- 二、常见的类型擦除容器
- 1.std::any
- 2.std::function
- 3.std::shared_ptr\<void\>和 std::unique_ptr\<void\>
- 4.总结
- 三、实现一个any
- 参考
类型擦除(Type Erasure)是一种编程技术,通过它可以在运行时存储和操作不同类型的对象,同时隐藏这些类型的具体信息。C++ 标准库中提供了几种常见的类型擦除容器,用于存储和操作多态对象。这些容器包括但不限于以下几种:
- std::any(C++17 引入)
- std::function
- std::shared_ptr 和 std::unique_ptr
- boost::any(Boost 库)
- boost::function(Boost 库)
一、类型擦除的作用
类型擦除就是将原有类型消除或者隐藏。
为什么要擦除类型?因为很多时候我不关心具体类型是什么或者根本就不需要这个类型,通过类型擦除我们可以获取很多好处,比如使得我们的程序有更好的扩展性、还能消除耦合以及消除一些重复行为,使程序更加简洁高效。
归纳一下c++中类型擦除方式主要有如下五种:
- 第一种:通过多态来擦除类型
- 第二种:通过模板来擦除类型
- 第三种:通过某种容器来擦除类型
- 第四种:通过某种通用类型来擦除类型
- 第五种:通过闭包来擦除类型
第一种类型隐藏的方式最简单也是我们经常用的,通过将派生类型隐式转换成基类型,再通过基类去多态的调用行为,在这种情况下,我不用关心派生类的具体类型,我只需要以一种统一的方式去做不同的事情,所以就把派生类型转成基类型隐藏起来,这样不仅仅可以多态调用还使我们的程序具有良好的可扩展性。然而这种方式的类型擦除仅仅是部分的类型擦除,因为基类型仍然存在,而且这种类型擦除的方式还必须是继承方式的才可以,而且继承使得两个对象强烈的耦合在一起了,正是因为这些缺点,通过多态来擦除类型的方式有较多局限性效果也不好。
时我们通过第二种方式擦除类型,以解决第一种方式的一些缺点。通过模板来擦除类型,本质上是把不同类型的共同行为进行了抽象,这时不同类型彼此之间不需要通过继承这种强耦合的方式去获得共同的行为了,仅仅是通过模板就能获取共同行为,降低了不同类型之间的耦合,是一种很好的类型擦除方式。然而,第二种方式虽然降低了对象间的耦合,但是还有一个问题没解决,就是基本类型始终需要指定,并没有消除基本类型,例如,我不可能把一个T本身作为容器元素,必须在容器初始化时就要知名这个T是具体某个类型。
需要注意的是,第四和第五种方式虽然解决了第三种方式不能彻底消除基本类型的缺点,但是还存一个缺点,就是取值的时候仍然依赖于具体类型,无论我是通过get还是any_cast,我都要T的具体类型,这在某种情况下仍然有局限性。
eg:我有A、B、C、D四种结构体,每个结构体中有某种类型的指针,名称且称为info,我现在提供了返回这些结构体的四个接口供外接使用,有可能是c#或者dephi调用这些接口,由于结构体中的info指针是我分配的内存,所以我必须提供释放这些指针的接口。代码如下:
struct A
{int* info;int id;
};struct B
{double* info;int id;
};struct C
{char* info;int id;
};struct D
{float* info;int id;
};//对外提供的删除接口
void DeleteA(A& t)
{delete t.info;
}void DeleteB(B& t)
{delete t.info;
}void DeleteC(C& t)
{delete t.info;
}void DeleteD(D& t)
{delete t.info;
}
大家可以看到,增加的四个删除函数内部都是重复代码,本来通过模板函数一行搞定,但是没办法,c#可没有c++的模板,还得老老实实的提供这些重复行为的接口,而且这种方式还有个坏处就是每增加一种类型就得增加一个重复的删除接口,怎么办?能统一成一个删除接口吗?可以,一个可行的办法就是将分配的内存通过一个ID关联并保存起来,让外接传一个ID,告诉我要删那块内存,新的统一删除函数可能是这样:
//内部将分配的内存存到map中,让外面传ID,内部通过ID去删除对应的内存块
map<int, T> mapT;template<typename R, typename T>
R GetT()
{R result{1,new T()};mapT.insert(std::pair<int, T>(1, R)); return result;
}//通过ID去关联我分配的内存块,外面传ID,内部通过ID去删除关联的内存块
void DeleteT(const int& id)
{R t = mapT[id]->second();delete t.info;
}
很遗憾,上面的代码编译不过,因为,map<int, T> mapT只能保存一种类型的对象,无法把分配的不同类型的对象保存起来,我们可以通过方式三和方式四,用variant或者any去擦除类型,解决T不能代表多种类型的问题,第一个问题解决。但是还有第二个问题,DeleteT时,从map中返回的variant或者any,无法取出来,因为接口函数中没有类型信息,而取值方法get和any_cast都需要一个具体类型。似乎进入了死胡同,无法只提供一个删除接口了。但是办法总还是有的。
方式五隆重登场了,看似无解的问题,通过方式五就能解决了。通过闭包来擦除类型很好很强大。
闭包也可以称为匿名函数或者lamda表达式,c++11中的lamda表达式就是c++中的闭包,c++11引入lamda,实际上引入了函数式编程的概念,函数式编程有很多优点,使代码更简洁,而且声明式的编码方式更贴近人的思维方式。函数式编程在更高的层次上对不同类型的公共行为进行了抽象,从而使我们不必去关心具体类型。
std::map < int, std::function <void()>> m_freeMap; //保存返回出去的内存块template<typename R, typename T>
R GetResult()
{R result = GetTable<R, T>(); m_freeMap.insert(std::make_pair(result.sequenceId, [this, result]{FreeResult(result); }));
}bool FreeResultById(int& memId){auto it = m_freeMap.find(memId);if (it == m_freeMap.end())return false;it->second(); //delete by lamdam_freeMap.erase(memId);return true;}
通过闭包去擦除类型,可以解决前面四种擦除方式遇到的问题
二、常见的类型擦除容器
1.std::any
std::any 是 C++17 引入的一种类型擦除容器,可以存储任何类型的值,并在运行时决定具体类型。
#include <any>
#include <iostream>
#include <string>int main() {std::any a = 42;std::cout << std::any_cast<int>(a) << std::endl;a = std::string("Hello, World!");std::cout << std::any_cast<std::string>(a) << std::endl;return 0;
}
2.std::function
std::function 是一种通用的多态函数封装器,可以存储和调用任何可调用对象,包括函数指针、lambda 表达式、绑定表达式或其他函数对象。
#include <functional>
#include <iostream>void foo() {std::cout << "foo" << std::endl;
}int main() {std::function<void()> f = foo;f();f = []() { std::cout << "lambda" << std::endl; };f();return 0;
}
3.std::shared_ptr<void>和 std::unique_ptr<void>
智能指针 std::shared_ptr 和 std::unique_ptr 可以通过指向 void 类型来实现类型擦除,从而存储任意类型的对象。
#include <iostream>
#include <memory>int main() {std::shared_ptr<void> p = std::make_shared<int>(42);std::cout << *std::static_pointer_cast<int>(p) << std::endl;p = std::make_shared<std::string>("Hello, World!");std::cout << *std::static_pointer_cast<std::string>(p) << std::endl;return 0;
}
4.总结
这些类型擦除容器在需要存储和操作多态对象时非常有用,可以在运行时决定具体类型,而不需要在编译时知道类型的具体信息。根据具体需求和项目依赖,可以选择使用标准库或 Boost 库提供的类型擦除容器。
三、实现一个any
any的设计思路:Any内部维护了一个基类指针,通过基类指针擦除具体类型,any_cast时再通过向下转型获取实际数据。当转型失败时打印详情。
any能容纳所有类型的数据,因此当赋值给any时,需要将值的类型擦除才行,即以一种通用的方式保存所有类型的数据。这里可以通过继承去擦除类型,基类是不含模板参数的,派生类中才有模板参数,这个模板参数类型正是赋值的类型,在赋值时,将创建的派生类对象赋值给基类指针,基类的派生类中携带了数据类型,基类只是原始数据的一个占位符,通过多态,它擦除了原始数据类型,因此,任何数据类型都可以赋值给他,从而实现了能存放所有类型数据的目标。
当取数据时需要向下转换成派生类型来获取原始数据,当转换失败时打印详情,并抛出异常。
由于any赋值时需要创建一个派生类对象,所以还需要管理该对象的生命周期,这里用unique_ptr智能指针去管理对象的生命周期。
#include <iostream>
#include <string>
#include <memory>
#include <typeindex>
struct Any
{Any(void) : m_tpIndex(std::type_index(typeid(void))){}Any(const Any& that) : m_ptr(that.Clone()), m_tpIndex(that.m_tpIndex) {}Any(Any && that) : m_ptr(std::move(that.m_ptr)), m_tpIndex(that.m_tpIndex) {}//创建智能指针时,对于一般的类型,通过std::decay来移除引用和cv符,从而获取原始类型template<typename U, class = typename std::enable_if<!std::is_same<typename std::decay<U>::type, Any>::value, U>::type> Any(U && value) : m_ptr(new Derived < typename std::decay<U>::type>(forward<U>(value))),m_tpIndex(type_index(typeid(typename std::decay<U>::type))){}bool IsNull() const { return !bool(m_ptr); }template<class U> bool Is() const{return m_tpIndex == type_index(typeid(U));}//将Any转换为实际的类型template<class U>U& AnyCast(){if (!Is<U>()){cout << "can not cast " << typeid(U).name() << " to " << m_tpIndex.name() << endl;throw bad_cast();}auto derived = dynamic_cast<Derived<U>*> (m_ptr.get());return derived->m_value;}Any& operator=(const Any& a){if (m_ptr == a.m_ptr)return *this;m_ptr = a.Clone();m_tpIndex = a.m_tpIndex;return *this;}private:struct Base;typedef std::unique_ptr<Base> BasePtr;struct Base{virtual ~Base() {}virtual BasePtr Clone() const = 0;};template<typename T>struct Derived : Base{template<typename U>Derived(U && value) : m_value(forward<U>(value)) { }BasePtr Clone() const{return BasePtr(new Derived<T>(m_value));}T m_value;};BasePtr Clone() const{if (m_ptr != nullptr)return m_ptr->Clone();return nullptr;}BasePtr m_ptr;std::type_index m_tpIndex;
};
测试:
void TestAny()
{Any n; auto r = n.IsNull();//truestring s1 = "hello";n = s1;n = "world";n.AnyCast<int>(); //can not cast int to stringAny n1 = 1;n1.Is<int>(); //true
}
其他:
template<typename T>
T& any_cast(any& aAny)
{if (typeid(T) == aAny.type_){return *static_cast<T*>(aAny.data_);}else{throw std::bad_any_cast{};}
}
参考
- (原创)c++中的类型擦除
- How std::any Works
相关文章:
C++类型参数技术以及常见的类型擦除容器
文章目录 一、类型擦除的作用二、常见的类型擦除容器1.std::any2.std::function3.std::shared_ptr\<void\>和 std::unique_ptr\<void\>4.总结 三、实现一个any参考 类型擦除(Type Erasure)是一种编程技术,通过它可以在运行时存储…...
SpringBoot如何缓存方法返回值?
Why? 为什么要对方法的返回值进行缓存呢? 简单来说是为了提升后端程序的性能和提高前端程序的访问速度。减小对db和后端应用程序的压力。 一般而言,缓存的内容都是不经常变化的,或者轻微变化对于前端应用程序是可以容忍的。 否…...
C#的web项目ASP.NET
添加实体类和控制器类 using System; using System.Collections.Generic; using System.Linq; using System.Web;namespace WebApplication1.Models {public class Company{public string companyCode { get; set; }public string companyName { get; set; }public string com…...
Spring MVC 源码分析之 DispatcherServlet#getHandlerAdapter 方法
前言: 前面我们分析了 Spring MVC 的工作流程源码,其核心是 DispatcherServlet#doDispatch 方法,我们前面分析了获取 Handler 的方法 DispatcherServlet#getHandler 方法,本篇我们重点分析一下获取当前请求的适配器 HandlerAdapt…...
假设检验学习笔记
1. 假设检验的基本概念 1.1. 原假设(零假设) 对总体的分布所作的假设用表示,并称为原假设或零假设 在总体分布类型已知的情况下,仅仅涉及总体分布中未知参数的统计假设,称为参数假设 在总体分布类型未知的情况下&#…...
vue3 watch学习
watch的侦听数据源类型 watch的第一个参数为侦听数据源,有4种"数据源": ref(包括计算属性) reactive(响应式对象) getter函数 多个数据源组成的数组。 //ref const xref(0)//单个ref watch(x,(newX)>{console.…...
推荐的Pytest插件
推荐的Pytest插件 Pytest的插件生态系统非常丰富,以下是一些特别推荐的Pytest插件: pytest-sugar 这个插件改进了Pytest的默认输出,添加了进度条,并立即显示失败的测试。它不需要额外配置,只需安装即可享受更漂亮、更…...
C语言 | Leetcode C语言题解之第124题二叉树中的最大路径和
题目: 题解: /*** Definition for a binary tree node.* struct TreeNode {* int val;* struct TreeNode *left;* struct TreeNode *right;* };*/ int max; int dfs(struct TreeNode* root){if(!root) return 0;int left dfs(root->left…...
Linux综合实践(Ubuntu)
目录 一、配置任务 1.1 配置该服务器的软件源为中科大软件源 1.2 安装相关软件openssh-server和vim 1.3 设置双网卡,网卡1为NAT模式,网卡2为桥接模式(桥接模式下,使用静态ip,该网卡数据跟实验室主机网络设置相似,除…...
C++面试题其二
19. STL中unordered_map和map的区别 unordered_map 和 map 都是C标准库中的关联容器,但它们在实现和性能方面有显著区别: 底层实现:map 是基于红黑树实现的有序关联容器,而 unordered_map 是基于哈希表实现的无序关联容器。元素…...
系统架构设计师【第9章】: 软件可靠性基础知识 (核心总结)
文章目录 9.1 软件可靠性基本概念9.1.1 软件可靠性定义9.1.2 软件可靠性的定量描述9.1.3 可靠性目标9.1.4 可靠性测试的意义9.1.5 广义的可靠性测试与狭义的可靠性测试 9.2 软件可靠性建模9.2.1 影响软件可靠性的因素9.2.2 软件可靠性的建模方法9.2.3 软件的可靠性模…...
x264 参考帧管理原理:i_poc_type 变量
x264 参考帧管理 x264 是一个开源的 H.264 视频编码软件,它提供了许多高级特性,包括对参考帧的高效管理。参考帧管理是视频编码中的一个重要部分,它涉及到如何存储、更新和使用已经编码的帧以提高编码效率。 x264 参考帧管理的一些关键点总结如下: 参考帧的初始化和重排序:…...
高级Web Lab2
高级Web Lab2 12 1 按照“Lab 2 基础学习文档”文档完成实验步骤 实验截图: 2 添加了Web3D场景选择按钮,可以选择目标课程或者学习房间。...
Linux网络-使用Tcp协议进行网络通信并通过网络接口实现远端翻译
文章目录 Tcp协议Tcp协议常见API接口1. int socket(int domain, int type, int protocol);2. int bind(int socket, const struct sockaddr *address, socklen_t address_len);struct sockaddr 3. int listen(int socket, int backlog);4. int accept(int socket, struct socka…...
实时数据传输:Django 与 MQTT 的完美结合
文章目录 准备工作创建 Django 项目与应用设置 MQTT 服务器编写 Django 视图编写前端模板发布 MQTT 消息运行 Django 项目 在当今互联网应用中,实时数据传输已经成为许多项目的核心需求。无论是社交媒体平台、在线游戏、金融交易还是物联网设备,都需要及…...
创建Django项目及应用
1 创建Project 1个Project可以对应多个app django-admin startproject myproject 2 创建App python manage.py startapp app01 INSTALLED_APPS [# ...app01,app02,# ... ] 如果要让这个应用在项目中起作用,需要在项目的 settings.py 文件的 INSTALLED_APPS 配置…...
Flutter课程分享 -(系统课程 基础 -> 进阶 -> 实战 仿京东商城)
前言 在移动应用开发的世界中,Flutter 作为一款由 Google 推出的开源 UI 软件开发工具包,正迅速赢得开发者们的青睐。其跨平台、高性能、丰富的组件库以及易于学习的特性,使得 Flutter 成为许多开发者的不二选择。然而,对于初学者…...
IDEA 中导入脚手架后该如何处理?
MySQL数据库创建啥的,没啥要说的!自行配置即可! 1.pom.xml文件,右键,add Maven Project …………(将其添加为Maven)【下述截图没有add Maven Project 是因为目前已经是Maven了!&…...
thinkphp6 queue队列的maxTries自定义
前景需求:在我们用队列的时候发现maxtries的个数时255次,这个太影响其他队列任务 我目前使用的thinkphp版本是6.1 第一部定义一个新的类 CustomDataBase(我用的mysql数据库存放的队列) 重写__make 和createPlainPayload方法 …...
【PHP项目实战训练】——laravel框架的实战项目中可以做模板的增删查改功能(2)
👨💻个人主页:开发者-曼亿点 👨💻 hallo 欢迎 点赞👍 收藏⭐ 留言📝 加关注✅! 👨💻 本文由 曼亿点 原创 👨💻 收录于专栏:…...
wordpress后台更新后 前端没变化的解决方法
使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…...
synchronized 学习
学习源: https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖,也要考虑性能问题(场景) 2.常见面试问题: sync出…...
论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(二)
HoST框架核心实现方法详解 - 论文深度解读(第二部分) 《Learning Humanoid Standing-up Control across Diverse Postures》 系列文章: 论文深度解读 + 算法与代码分析(二) 作者机构: 上海AI Lab, 上海交通大学, 香港大学, 浙江大学, 香港中文大学 论文主题: 人形机器人…...
Spark 之 入门讲解详细版(1)
1、简介 1.1 Spark简介 Spark是加州大学伯克利分校AMP实验室(Algorithms, Machines, and People Lab)开发通用内存并行计算框架。Spark在2013年6月进入Apache成为孵化项目,8个月后成为Apache顶级项目,速度之快足见过人之处&…...
3.3.1_1 检错编码(奇偶校验码)
从这节课开始,我们会探讨数据链路层的差错控制功能,差错控制功能的主要目标是要发现并且解决一个帧内部的位错误,我们需要使用特殊的编码技术去发现帧内部的位错误,当我们发现位错误之后,通常来说有两种解决方案。第一…...
ESP32 I2S音频总线学习笔记(四): INMP441采集音频并实时播放
简介 前面两期文章我们介绍了I2S的读取和写入,一个是通过INMP441麦克风模块采集音频,一个是通过PCM5102A模块播放音频,那如果我们将两者结合起来,将麦克风采集到的音频通过PCM5102A播放,是不是就可以做一个扩音器了呢…...
TRS收益互换:跨境资本流动的金融创新工具与系统化解决方案
一、TRS收益互换的本质与业务逻辑 (一)概念解析 TRS(Total Return Swap)收益互换是一种金融衍生工具,指交易双方约定在未来一定期限内,基于特定资产或指数的表现进行现金流交换的协议。其核心特征包括&am…...
拉力测试cuda pytorch 把 4070显卡拉满
import torch import timedef stress_test_gpu(matrix_size16384, duration300):"""对GPU进行压力测试,通过持续的矩阵乘法来最大化GPU利用率参数:matrix_size: 矩阵维度大小,增大可提高计算复杂度duration: 测试持续时间(秒&…...
在WSL2的Ubuntu镜像中安装Docker
Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包: for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...
网络编程(UDP编程)
思维导图 UDP基础编程(单播) 1.流程图 服务器:短信的接收方 创建套接字 (socket)-----------------------------------------》有手机指定网络信息-----------------------------------------------》有号码绑定套接字 (bind)--------------…...
