1. 类型擦除技术概述
在C++开发中,类型擦除(Type Erasure)是一种强大的编程技术,它允许我们编写能够处理多种不同类型的代码,同时保持静态类型安全。这种技术在现代C++库中随处可见,比如标准库中的std::function、std::any等。
类型擦除的核心思想是:通过某种方式"擦除"具体类型信息,使得不同类型的对象可以用统一的方式处理。听起来有点像动态类型语言的特征?但C++的类型擦除是在编译期完成的,完全保留了静态类型检查的优势。
我第一次在实际项目中使用类型擦除是为了解决一个插件系统的问题。我们需要加载第三方插件,这些插件实现了相同的接口但来自不同的编译单元。类型擦除完美地解决了这个问题,让我从此对这种技术产生了浓厚的兴趣。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 类型擦除的实现原理
2.1 基于继承的经典实现
最传统的类型擦除实现方式是使用继承和多态。我们定义一个抽象基类作为接口,然后用模板化的派生类来包装具体类型:
cpp复制class AnyDrawable {
public:
virtual ~AnyDrawable() = default;
virtual void draw() const = 0;
};
template <typename T>
class DrawableImpl : public AnyDrawable {
public:
DrawableImpl(T obj) : obj_(std::move(obj)) {}
void draw() const override { obj_.draw(); }
private:
T obj_;
};
这种方式的优点是直观易懂,缺点是会有虚函数调用的开销,而且需要堆分配内存。
2.2 基于函数指针的实现
更高效的实现可以避免虚函数调用,直接使用函数指针:
cpp复制class AnyDrawable {
public:
template <typename T>
AnyDrawable(T obj) :
obj_(new T(std::move(obj))),
draw_([](void* obj) { static_cast<T*>(obj)->draw(); }) {}
void draw() const { draw_(obj_.get()); }
private:
std::unique_ptr<void, void(*)(void*)> obj_;
void (*draw_)(void*);
};
这种方式性能更好,但实现起来更复杂,需要手动管理类型转换。
2.3 小对象优化技术
为了优化性能,我们可以结合小对象优化(Small Object Optimization),避免小对象的堆分配:
cpp复制class AnyDrawable {
static constexpr size_t BufferSize = 64;
struct Concept {
virtual ~Concept() = default;
virtual void draw() const = 0;
};
template <typename T>
struct Model final : Concept {
Model(T value) : value(std::move(value)) {}
void draw() const override { value.draw(); }
T value;
};
union {
std::aligned_storage_t<BufferSize> buffer;
Concept* ptr;
} storage;
bool isOnHeap;
public:
template <typename T>
AnyDrawable(T value) {
if (sizeof(Model<T>) <= BufferSize) {
new (&storage.buffer) Model<T>(std::move(value));
isOnHeap = false;
} else {
storage.ptr = new Model<T>(std::move(value));
isOnHeap = true;
}
}
~AnyDrawable() {
if (isOnHeap) {
delete storage.ptr;
} else {
getConcept()->~Concept();
}
}
void draw() const { getConcept()->draw(); }
private:
Concept* getConcept() const {
return isOnHeap ? storage.ptr :
reinterpret_cast<Concept*>(&storage.buffer);
}
};
这种实现对于小型对象(小于BufferSize)直接在栈上分配,避免了堆分配的开销,是标准库中常用的一种优化技术。
3. 类型擦除的实际应用
3.1 std::function的实现
标准库中的std::function就是一个典型的类型擦除应用。它可以存储任何可调用对象,无论这个对象是函数指针、成员函数指针、lambda表达式还是函数对象。
cpp复制std::function<void(int)> f;
f = [](int x) { std::cout << x << std::endl; }; // lambda
f = &someFunction; // 函数指针
f = std::bind(&SomeClass::method, &obj, std::placeholders::_1); // 成员函
