跟我学c++中级篇——类型擦除

跟我学c++中级篇——类型擦除一 类型擦除 很多人一直都认为 类型擦除是一些高级语言 如 Java 才具有的 其实在 c 中也可以实现类型擦除 那么什么是类型擦除呢 我们都知道 C c 是一门强类型语言 也就是说 编译器必须知道数据是属于什么类型的 说的直白一点

大家好,我是讯享网,很高兴认识大家。

一、类型擦除

很多人一直都认为,类型擦除是一些高级语言(如Java)才具有的,其实在c++中也可以实现类型擦除。那么什么是类型擦除呢?我们都知道,C/c++是一门强类型语言,也就是说,编译器必须知道数据是属于什么类型的。说的直白一点,就是int类型还是其它什么类型,当然这些类型里也包括对象类型。而类型擦除,就是要把这些数据的类型抹去,或者说擦除掉,当然也可以理解为隐藏掉数据的类型。这不和刚刚说的强类型语言相反么,这样做有什么用处呢?
做为强类型语言,所有的数据类型必须强调可知就会有一个显示的问题,无法用一个通过的定义来描述这些数据类型非特定的行为。而往往是这些行为决定了设计上的解耦和分离。而这种行为的不同就是程序设计可扩展性和简洁高效的前提和基础,它可以显著的降低程序中的侵入式设计。而在前面也反复提到过,c++从设计上本身是无法提供在运行时动态获取数据对象的类型的功能的。这也让类型擦除更具重要性。
这里需要提到一个定义:鸭子类型(英语:duck typing)是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由“当前方法和属性的集合”决定。这玩意儿有啥用呢,它可以不通过继承实现多态。

二、类型擦除的方式

那么数据类型有几种方式呢,常见的有以下几种:
1、多态
多态一般都很了解,可以通过父类型指针来管理子类型的指针,实现了对子对象类型的隐藏。但这种方式的缺点也非常明显,需要写继承的类而且数据类型的隐藏并不全面,至少父类型始终要暴露出来。
2、模板
模板在前面也提到过多次,通过模板,可以实现对多种类型的行为操作进行抽象。比如一个模板的加法函数(比如多个类都调用此加法函数),可以实现对不同的数据类型的操作,如果对象也重载了加法,同样也可以使用。不过,在基本类型中,这种模板函数抽象的行为就无能为力了。
3、容器
容器之所以可以擦除,其实更类似于模板,这个不用多说。另外还有一种组合容器,其实有些类似共用体。比如std::variant 。但此种方式其实也明确了数据的类型,只是多了几种罢了,让编译器在真正使用时进行选择。所以这种类型擦除更接近一种类型判断。
4、通用类型
这个就得提到前面分析的std::any,通过它可以进行数据类型的动态转换。但是其才应用时,仍然需要指定具体的类型后才能使用。
5、闭包
所谓闭包,其实就是Lambda表达式,通过它和std::function的配合,实现类型的擦除。

从上面的分析可以看到,其实具体实现上来看,如果想实现的比较理想(如any等的实现基本就是有限定范围的),基本上都需要两个基本条件,一个是带约束的模板构造函数,另外一个就是函数指针。再回头和SBO系列相对比一下就可以明白为啥std::function的实现机制中的基本方式了。同时,也可以搞明白,其实类型擦除就是一种使用函数指针来实现多态的机制。

三、应用场景


讯享网

四、例程

这里的例程使用一个同学的例程,大家参考分析一下就明白了。

 class MyFunction { private: class FunctorWrapper { public: virtual ~FunctorWrapper() = default; virtual FunctorWrapper* clone() const = 0; virtual void call() const = 0; }; template<typename T> class ConcreteWrapper : public FunctorWrapper { public: ConcreteWrapper(const T& functor) : functor(functor) { } virtual ~ConcreteWrapper() override = default; virtual ConcreteWrapper* clone() const { return new ConcreteWrapper(*this); } virtual void call() const override { functor(); } private: T functor; }; public: MyFunction() = default; template<typename T> MyFunction(T&& functor) : ptr(new ConcreteWrapper<T>(functor)) { } MyFunction(const MyFunction& other) : ptr(other.ptr->clone()) { } MyFunction& operator=(const MyFunction& other) { if (this != &other) { delete ptr; ptr = other.ptr->clone(); } return *this; } MyFunction(MyFunction&& other) noexcept : ptr(std::exchange(other.ptr, nullptr)) { } MyFunction& operator=(MyFunction&& other) noexcept { if (this != &other) { delete ptr; ptr = std::exchange(other.ptr, nullptr); } return *this; } ~MyFunction() { delete ptr; } void operator()() const { if (ptr) ptr->call(); } FunctorWrapper* ptr = nullptr; }; 

讯享网

再看一下不使用继承的:

讯享网class MyFunction { private: static constexpr std::size_t size = 16; static_assert(size >= sizeof(void*), ""); struct Data { Data() = default; char dont_use[size]; } data; template<typename T> static void functorConstruct(Data& dst, T&& src) { using U = typename std::decay<T>::type; if (sizeof(U) <= size) new ((U*)&dst) U(std::forward<U>(src)); else *(U)&dst = new U(std::forward<U>(src)); } template<typename T> static void functorDestructor(Data& data) { using U = typename std::decay<T>::type; if (sizeof(U) <= size) ((U*)&data)->~U(); else delete *(U)&data; } template<typename T> static void functorCopyCtor(Data& dst, const Data& src) { using U = typename std::decay<T>::type; if (sizeof(U) <= size) new ((U*)&dst) U(*(const U*)&src); else *(U)&dst = new U((const U)&src); } template<typename T> static void functorMoveCtor(Data& dst, Data& src) { using U = typename std::decay<T>::type; if (sizeof(U) <= size) new ((U*)&dst) U(*(const U*)&src); else *(U)&dst = std::exchange(*(U)&src, nullptr); } template<typename T> static void functorInvoke(const Data& data) { using U = typename std::decay<T>::type; if (sizeof(U) <= size) (*(U*)&data)(); else ((U)&data)(); } template<typename T> static void (*const vtables[4])(); void (*const* vtable)() = nullptr; public: MyFunction() = default; template<typename T> MyFunction(T&& obj) : vtable(vtables<T>) { functorConstruct(data, std::forward<T>(obj)); } MyFunction(const MyFunction& other) : vtable(other.vtable) { if (vtable) ((void (*)(Data&, const Data&))vtable[1])(this->data, other.data); } MyFunction& operator=(const MyFunction& other) { this->~MyFunction(); vtable = other.vtable; new (this) MyFunction(other); return *this; } MyFunction(MyFunction&& other) noexcept : vtable(std::exchange(other.vtable, nullptr)) { if (vtable) ((void (*)(Data&, Data&))vtable[2])(this->data, other.data); } MyFunction& operator=(MyFunction&& other) noexcept { this->~MyFunction(); new (this) MyFunction(std::move(other)); return *this; } ~MyFunction() { if (vtable) ((void (*)(Data&))vtable[0])(data); } void operator()() const { if (vtable) ((void (*)(const Data&))vtable[3])(this->data); } }; template<typename T> void (*const MyFunction::vtables[4])() = { (void (*)())MyFunction::functorDestructor<T>, (void (*)())MyFunction::functorCopyCtor<T>, (void (*)())MyFunction::functorMoveCtor<T>, (void (*)())MyFunction::functorInvoke<T>, }; 

有一个可能约束的模板构造函数和函数指针。上面的封装需要相关的模板类型实现了拷贝和仿函数的功能,否则不能使用。更多的可以参考一下原文:
https://www.cnblogs.com/jerry-fuyi/p/12664787.html#sbo
正所谓长江后浪推前浪,这位同学的博客质量相当高,大家可以看一看。
也可以参考:
https://quuxplusone.github.io/blog/2019/03/18/what-is-type-erasure/
https://zhuanlan.zhihu.com/p/
https://zhuanlan.zhihu.com/p/
https://zhuanlan.zhihu.com/p/
一般来说,在一元类型操作中(换句话说就是函数中一般只有一个模板参数)表现很良好,而有多个选项的话,则不太友好。

五、总结

小讯
上一篇 2025-02-11 20:55
下一篇 2025-03-17 10:42

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/33293.html