当前位置: 首页 > news >正文

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
http://www.lryc.cn/news/359310.html

相关文章:

  • SpringBoot如何缓存方法返回值?
  • C#的web项目ASP.NET
  • Spring MVC 源码分析之 DispatcherServlet#getHandlerAdapter 方法
  • 假设检验学习笔记
  • vue3 watch学习
  • 推荐的Pytest插件
  • C语言 | Leetcode C语言题解之第124题二叉树中的最大路径和
  • Linux综合实践(Ubuntu)
  • C++面试题其二
  • 系统架构设计师【第9章】: 软件可靠性基础知识 (核心总结)
  • x264 参考帧管理原理:i_poc_type 变量
  • 高级Web Lab2
  • Linux网络-使用Tcp协议进行网络通信并通过网络接口实现远端翻译
  • 实时数据传输:Django 与 MQTT 的完美结合
  • 创建Django项目及应用
  • Flutter课程分享 -(系统课程 基础 -> 进阶 -> 实战 仿京东商城)
  • IDEA 中导入脚手架后该如何处理?
  • thinkphp6 queue队列的maxTries自定义
  • 【PHP项目实战训练】——laravel框架的实战项目中可以做模板的增删查改功能(2)
  • Kotlin 对象
  • 力扣 142题 环形链表Ⅱ 记录
  • 乐观锁 or 悲观锁 你怎么选?
  • 《庆余年算法番外篇》:范闲通过最短路径算法在阻止黑骑截杀林相
  • 大一C语言课设 服装销售系统 代码实现与项目总结
  • 从新手到专家:深入探索JVM垃圾回收--开端篇
  • R可视化:另类的柱状图
  • Docker的数据管理(数据卷+数据卷容器)
  • 字符串-至多包含K种字符的子串中最长子串(mid)
  • Docker从安装开始精通
  • MFC:初步理解序列化与反序列化(含代码实现)