类型擦除类的实现中总会保留一点类型信息。MyFunction类中关于T的类型信息表现在FunctorWrapper的vtable中,本质上是函数指针。类型擦除类也可以跳过继承的工具,直接使用函数指针实现多态。无论
使用哪种实现,类型擦除类总是可以被拷贝或移动或两者兼有,多态性可以由对象本身体现。
不是每一滴牛奶都叫特仑苏,也不是每一个类的实例都能被MyFunction包装。MyFunction对T的要求是可以拷贝、可以用operator()() const调用,这些称为类型T的“affordance”。说到affordance,普通的模板函
数也对模板类型有affordance,比如std::sort要求迭代器可以随机存取,否则编译器会给你一堆冗长的错误信息。C++20引入了concept和requires子句,对编译器和程序员都是有好处的。
每个类型擦除类的affordance都在写成的时候确定下来。affordance被要求的方式不是继承某个基类,而只看你这个类是否有相应的方法,就像Python那样,只要函数接口匹配上就可以了。这种类型识别
方式称为“duck typing”,来源于“duck test”,意思是“If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck”。
类型擦除类要求的affordance通常都是一元的,也就是成员函数的参数中不含T,比如对于包装整数的类,你可以要求T + 42,但是无法要求T + U,一个类型擦除类的实例是不知道另一个属于同一个类但是
构造自不同类型对象的实例的信息的。我觉得这条规则有一个例外,operator==是可以想办法支持的。
MyFunction类虽然实现了值多态,但还是使用了new和delete语句。如果可调用对象只是一个简单的函数指针,是否有必要在堆上开辟空间?
SBO
小的对象保存在类实例中,大的对象交给堆并在实例中维护指针,这种技巧称为小缓冲优化(Small Buffer Optimization, SBO)。大多数类型擦除类都应该使用SBO以节省内存并提升效率,问题在于
SBO与继承不共存,维护每个实例中的一个vtable或几个函数指针是件挺麻烦的事,还会拖慢编译速度。
但是在内存和性能面前,这点工作量能叫事吗?
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])() =