很多人一直都认为,类型擦除是一些高级语言(如Java)才具有的,其实在c++中也可以实现类型擦除。那么什么是类型擦除呢?我们都知道,C/c++是一门强类型语言,也就是说,编译器必须知道数据是属于什么类型的。说的直白一点,就是int类型还是其它什么类型,当然这些类型里也包括对象类型。而类型擦除,就是要把这些数据的类型抹去,或者说擦除掉,当然也可以理解为隐藏掉数据的类型。这不和刚刚说的强类型语言相反么,这样做有什么用处呢?
做为强类型语言,所有的数据类型必须强调可知就会有一个显示的问题,无法用一个通过的定义来描述这些数据类型非特定的行为。而往往是这些行为决定了设计上的解耦和分离。而这种行为的不同就是程序设计可扩展性和简洁高效的前提和基础,它可以显著的降低程序中的侵入式设计。而在前面也反复提到过,c++从设计上本身是无法提供在运行时动态获取数据对象的类型的功能的。这也让类型擦除更具重要性。
这里需要提到一个定义:鸭子类型(英语:duck typing)是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由“当前方法和属性的集合”决定。这玩意儿有啥用呢,它可以不通过继承实现多态。
那么数据类型有几种方式呢,常见的有以下几种:
1、多态
多态一般都很了解,可以通过父类型指针来管理子类型的指针,实现了对子对象类型的隐藏。但这种方式的缺点也非常明显,需要写继承的类而且数据类型的隐藏并不全面,至少父类型始终要暴露出来。
2、模板
模板在前面也提到过多次,通过模板,可以实现对多种类型的行为操作进行抽象。比如一个模板的加法函数(比如多个类都调用此加法函数),可以实现对不同的数据类型的操作,如果对象也重载了加法,同样也可以使用。不过,在基本类型中,这种模板函数抽象的行为就无能为力了。
3、容器
容器之所以可以擦除,其实更类似于模板,这个不用多说。另外还有一种组合容器,其实有些类似共用体。比如std::variant 。但此种方式其实也明确了数据的类型,只是多了几种罢了,让编译器在真正使用时进行选择。所以这种类型擦除更接近一种类型判断。
4、通用类型
这个就得提到前面分析的std::any,通过它可以进行数据类型的动态转换。但是其才应用时,仍然需要指定具体的类型后才能使用。
5、闭包
所谓闭包,其实就是Lambda表达式,通过它和std::function的配合,实现类型的擦除。
从上面的分析可以看到,其实具体实现上来看,如果想实现的比较理想(如any等的实现基本就是有限定范围的),基本上都需要两个基本条件,一个是带约束的模板构造函数,另外一个就是函数指针。再回头和SBO系列相对比一下就可以明白为啥std::function的实现机制中的基本方式了。同时,也可以搞明白,其实类型擦除就是一种使用函数指针来实现多态的机制。
类型擦除的应用场景非常多,特别是在设计中,如何实现一个非继承方式来搞定扩展而勿需考虑数据类型时,都可以使用这种方式。当然,如果看过MFC实现的源码,也可以使用类似其侵入式的设计来实现,但那个太复杂了,而且可扩展性也有限。业界现在基本已经不在采用那种方式来设计程序。
另外上一篇中SBO系列中,其实也是采用了这种方式来实现。只要能够明白函数指针的机制,这种设计就可以不拘泥于某种场景,自由的发挥。而所谓类型擦除不过是其中一个应用的重要实现而已。
这里的例程使用一个同学的例程,大家参考分析一下就明白了。
class MyFunction
{
private:
class FunctorWrapper
{
public:
virtual ~FunctorWrapper() = default;
virtual FunctorWrapper* clone() const = 0;
virtual void call() const = 0;
};
template
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
MyFunction(T&& functor)
: ptr(new ConcreteWrapper(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
static void functorConstruct(Data& dst, T&& src)
{
using U = typename std::decay::type;
if (sizeof(U) <= size)
new ((U*)&dst) U(std::forward(src));
else
*(U**)&dst = new U(std::forward(src));
}
template
static void functorDestructor(Data& data)
{
using U = typename std::decay::type;
if (sizeof(U) <= size)
((U*)&data)->~U();
else
delete *(U**)&data;
}
template
static void functorCopyCtor(Data& dst, const Data& src)
{
using U = typename std::decay::type;
if (sizeof(U) <= size)
new ((U*)&dst) U(*(const U*)&src);
else
*(U**)&dst = new U(**(const U**)&src);
}
template
static void functorMoveCtor(Data& dst, Data& src)
{
using U = typename std::decay::type;
if (sizeof(U) <= size)
new ((U*)&dst) U(*(const U*)&src);
else
*(U**)&dst = std::exchange(*(U**)&src, nullptr);
}
template
static void functorInvoke(const Data& data)
{
using U = typename std::decay::type;
if (sizeof(U) <= size)
(*(U*)&data)();
else
(**(U**)&data)();
}
template
static void (*const vtables[4])();
void (*const* vtable)() = nullptr;
public:
MyFunction() = default;
template
MyFunction(T&& obj)
: vtable(vtables)
{
functorConstruct(data, std::forward(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
void (*const MyFunction::vtables[4])() =
{
(void (*)())MyFunction::functorDestructor,
(void (*)())MyFunction::functorCopyCtor,
(void (*)())MyFunction::functorMoveCtor,
(void (*)())MyFunction::functorInvoke,
};
有一个可能约束的模板构造函数和函数指针。上面的封装需要相关的模板类型实现了拷贝和仿函数的功能,否则不能使用。更多的可以参考一下原文:
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/351291649
https://zhuanlan.zhihu.com/p/351464404
https://zhuanlan.zhihu.com/p/374562057
一般来说,在一元类型操作中(换句话说就是函数中一般只有一个模板参数)表现很良好,而有多个选项的话,则不太友好。
现在大家都很惶惑,不知道未来的方向。可能抬眼望去,都是不可预知的迷茫。越是在这个时候儿,越是要沉住气,要不断的锤炼自己的技术,趁着这个机会把基础打得更牢更扎实;同时,要不断的寻找方向,不要因为找不到方向就焦虑,因为可能下一次就会发现正确的方向。没有方向要大胆去找,有了方向要大胆去试。
特别是年青人,现在属于你们的世界。努力挥洒汗水,光明就在拐角。