类型擦除让 C++ 在运行时处理未知类型。核心:隐藏具体类型,只暴露接口。标准库如 std::function, std::any, std::variant 都依赖它。

类型擦除是什么?

定义:运行时统一处理多种类型,编译时忘掉具体类型。只剩接口。

解决痛点

  • 模板太静态,无法处理运行时类型(插件、网络数据)。
  • 容器存异构类型。
  • API 解耦:接受任意 callable。

核心机制:动态多态 + vtable。

实现原理

三件套

  • 接口:纯虚基类。
  • Holder:模板派生,持值。
  • 包装器:持 Base*,转发调用。

vs 模板:模板是静态分发,零运行时开销;类型擦除是动态分发,有虚调用和堆分配成本。
vs variantstd::variant 是静态类型擦除,编译期已知所有类型,无堆分配。

标准库示例

std::any (C++17)

简化实现(核心:vtable + SBO):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Any {
struct VTable {
void* (*clone)(const void* src, void* dst);
void (*destroy)(void* obj);
const std::type_info& (*type)();
};

alignas(std::max_align_t) char buf_[16]; // 小对象缓冲区
void* ptr_ = nullptr;
const VTable* vt_ = nullptr;

template<typename T>
static const VTable* get_vtable() {
static const VTable vt = {
[](const void* src, void* dst) -> void* {
if constexpr (sizeof(T) <= sizeof(buf_)) {
return new(dst) T(*static_cast<const T*>(src));
} else {
return new T(*static_cast<const T*>(src));
}
},
[](void* obj) {
if constexpr (sizeof(T) <= sizeof(buf_)) {
static_cast<T*>(obj)->~T();
} else {
delete static_cast<T*>(obj);
}
},
[]() -> const std::type_info& { return typeid(T); }
};
return &vt;
}

public:
template<typename T>
Any(T&& val) {
using DT = std::decay_t<T>;
vt_ = get_vtable<DT>();
if constexpr (sizeof(DT) <= sizeof(buf_)) {
ptr_ = buf_;
new(ptr_) DT(std::forward<T>(val));
} else {
ptr_ = new DT(std::forward<T>(val));
}
}

template<typename T>
T* cast() {
if (vt_->type() != typeid(T)) throw std::bad_any_cast{};
return static_cast<T*>(ptr_);
}

~Any() { if (vt_) vt_->destroy(ptr_); }
};

要点:vtable 管理类型相关操作;SBO 让小对象走栈,避免堆分配。

std::function (C++11)

统一 callable,核心:类型擦除 + vtable + SBO。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
template<typename Ret, typename... Args>
class Function {
struct VTable {
void (*destroy)(void*);
void (*clone)(const void*, void*);
Ret (*call)(void*, Args...);
};

alignas(16) char buf_[32];
void* ptr_ = nullptr;
const VTable* vt_ = nullptr;

template<typename F>
static constexpr bool CanSBO = sizeof(F) <= sizeof(buf_);

template<typename F>
static const VTable* get_vtable() {
static const VTable vt = {
[](void* p) { static_cast<F*>(p)->~F(); },
[](const void* src, void* dst) {
new(dst) F(*static_cast<const F*>(src));
},
[](void* p, Args... args) -> Ret {
return (*static_cast<F*>(p))(std::forward<Args>(args)...);
}
};
return &vt;
}

public:
template<typename F>
Function(F&& f) {
vt_ = get_vtable<F>();
if constexpr (CanSBO<F>) {
new(buf_) F(std::forward<F>(f));
} else {
ptr_ = new F(std::forward<F>(f));
}
}

Ret operator()(Args... args) {
void* storage = ptr_ ? ptr_ : buf_;
return vt_->call(storage, std::forward<Args>(args)...);
}

~Function() {
void* storage = ptr_ ? ptr_ : buf_;
vt_->destroy(storage);
delete static_cast<char*>(ptr_);
}
};

调用流程f(5) → 选 storage(buf_ / ptr_)→ vt_->call → 执行实际 callable。

其他类型擦除std::shared_ptr/std::unique_ptr 也算简单擦除——指针类型统一,指向任意对象。

手动实现 Any(经典模式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Any {
struct Concept {
virtual ~Concept() = default;
virtual Concept* clone() const = 0;
virtual const std::type_info& type() const = 0;
};

template<typename T>
struct Holder : Concept {
T val_;
Holder(T&& v) : val_(std::forward<T>(v)) {}
Concept* clone() const override { return new Holder(T{val_}); }
const std::type_info& type() const override { return typeid(T); }
};

std::unique_ptr<Concept> ptr_;

public:
template<typename T>
Any(T&& v) : ptr_(new Holder<T>(std::forward<T>(v))) {}

Any(const Any& other) : ptr_(other.ptr_ ? other.ptr_->clone() : nullptr) {}
Any(Any&&) noexcept = default;

template<typename T>
T& get() {
auto* h = dynamic_cast<Holder<T>*>(ptr_.get());
if (!h) throw std::bad_cast{};
return h->val_;
}
};

关键点Concept 擦除类型;Holder<T> 保留值;clone() 实现深拷贝;dynamic_cast 安全取回类型。

优缺点

优点缺点
运行时灵活(插件、事件、回调)性能损失:虚调用 + 堆分配
统一接口,少模板膨胀调试难:类型隐藏
解耦:调用方不依赖具体类型内存占用大

何时用:真有动态类型需求。能静态解决时,优先用模板或 std::variant

扩展

  • C++20 Concepts:可约束类型擦除的模板参数,提前拒绝不满足接口的类型。
  • CRTP:在需要静态多态时替代虚函数,减少运行时开销。
  • :标准库不够用时看 Boost.TypeErasure / Boost.Any

总结

  • 类型擦除 = 虚函数 + 模板桥接。
  • 标准库实现:vtable + SBO 优化。
  • 能用模板 / variant 就别用擦除。