1. 什么是类型擦除(Type Erasure)?
类型擦除是一种 C++ 设计模式,用于将不同类型通过统一接口进行封装,使得外部代码无需知道其真实类型。
换句话说:
让"不同类型"在运行期看起来像"同一种类型"。
与传统多态(继承 + 虚函数)不同:
| 比较点 | 类型擦除 | 虚继承多态 |
|---|---|---|
| 需要继承关系? | 不需要 | 需要同一基类 |
| 支持第三方类型? | 不修改代码即可封装 | 必须继承基类 |
| 运行时开销 | 小,灵活 | 虚表开销固定 |
| 表达能力 | 高(可实现多接口、强约束) | 一般 |
2. 为什么工程中强烈需要类型擦除?
假如现在要做的:
- 统一残差项接口(GTSAM/Ceres)
- 不同点云结构(PCL / 自定义 struct / Eigen 点)统一封装
- ICP / NDT / FPFH / GC-RANSAC 等模块统一接口
- 多传感器框架的模块化管理
这些都需要:
在不知道真实类型的情况下,存储对象、调用方法。
传统继承做不到(因为第三方类型不能随意继承),模板太死板(只能在编译期决定类型)。
类型擦除就是完美的解决工具。
3. 类型擦除的基本机制
类型擦除通常包含三部分:
[Concept] 抽象接口(你想要的 API)
[Model<T>] 对具体类型 T 的封装(实现 concept)
[Wrapper] 对外暴露:持有一个指针(指向 concept)
示意图:
┌───────────────┐
│ Wrapper │
│ holds Concept* │
└───────┬────────┘
│
┌─────────────┴─────────────┐
│ Concept(纯虚接口) │
└─────────────┬─────────────┘
│
┌────────────────┴───────────────┐
│ Model<T> │
│ 具体实现(包含真实 T 对象) │
└─────────────────────────────────┘
所以:
- 包装器存的是"有相同能力"的对象,而不是"相同类型"的对象。
4. 标准库中的类型擦除示例
C++ 标准库大量使用类型擦除:
| 类 | 擦除了什么类型? |
|---|---|
std::function |
任意可调用对象类型 |
std::any |
任意类型 |
std::shared_ptr<void> |
任意对象类型 |
std::ranges::view |
任意范围类型 |
例如 std::function<void()> 可以保存:
- 函数指针
- lambda
- functor
- bind 表达式
它根本不关心开发者的真实类型是什么。
5. 简单一个最小类型擦除示例
需求:想封装任意能执行 void operator()() 的对象。
Step 1 --- 定义 Concept(接口)
cpp
struct Concept {
virtual ~Concept() = default;
virtual void call() = 0;
};
Step 2 --- Model:对任意类型 T 包装
cpp
template <typename T>
struct Model : Concept {
T obj;
Model(T v) : obj(std::move(v)) {}
void call() override { obj(); }
};
Step 3 --- Wrapper:对外暴露的强类型安全的接口
cpp
class Function {
std::unique_ptr<Concept> self;
public:
template <typename T>
Function(T v) : self(std::make_unique<Model<T>>(std::move(v))) {}
void operator()() { self->call(); }
};
Step 4 --- 使用
cpp
void test() {
Function f1 = [](){ std::cout << "lambda\n"; };
Function f2 = std::bind([](int x){ std::cout << x << "\n"; }, 123);
f1();
f2();
}
输出:
lambda
123
6. 进一步扩展:多方法、多能力的类型擦除(SLAM场景中)
在 SLAM 中,一个模块可能需要多个接口:
void setup()void process(PointCloud&)double cost() const
那么 concept 只是多加几个方法即可:
cpp
struct Concept {
virtual ~Concept() = default;
virtual void setup() = 0;
virtual void process(PointCloud&) = 0;
virtual double cost() const = 0;
};
然后 Model:
cpp
template <typename T>
struct Model : Concept {
T obj;
void setup() override { obj.setup(); }
void process(PointCloud& pc) override { obj.process(pc); }
double cost() const override { return obj.cost(); }
};
Wrapper:
cpp
class Module {
std::unique_ptr<Concept> self;
public:
template <typename T>
Module(T v) : self(std::make_unique<Model<T>>(std::move(v))) {}
void setup() { self->setup(); }
void process(PointCloud& pc) { self->process(pc); }
double cost() const { return self->cost(); }
};
7. 实战:构建一个 SLAM 框架
假设现在的需求如下:
多类型点云处理模块统一接口
cpp
Module icp = ICPModule{};
Module ndt = NDTModule{};
Module surfel = SurfelMapper{};
for (auto& m : modules)
m.process(pointcloud);
无需继承,不需模板,只需满足接口。
多类型残差项(GTSAM / Ceres 接口统一)
可以如下实现:
cpp
Residual r = ReprojectionResidual(params);
Residual r2 = IMUPreintegrationResidual(params);
Residual r3 = LiDARPointEdgeResidual(params);
optimizer.addResidual(r);
任意传感器输入类型统一管理(Type-Erased Sensor)
cpp
Sensor s1 = CameraSensor{};
Sensor s2 = LidarSensor{};
Sensor s3 = IMUSensor{};
s1.read();
s2.read();
s3.read();
8. 进阶:小对象优化(Small Buffer Optimization, SBO)
为了减少动态分配开销,可在 wrapper 内部做 SBO:
cpp
static constexpr size_t BUF_SIZE = 64;
using Buffer = std::aligned_storage_t<BUF_SIZE, alignof(void*)>;
Buffer buffer;
Concept* self; // 指向 buffer 内部或 heap 分配
这种技术被用于:
std::functionstd::anyLLVM的 type-erased containers
可以使算法框架运行更快。详细内容我们在下节内容展开。
9. 进阶:无虚函数的类型擦除(C++20 Concepts + 函数指针表)
更高性能版本:
- 不用虚函数
- 不分配内存
- 用函数指针表(vtable)
示例:
cpp
struct VTable {
void (*setup)(void*);
void (*process)(void*, PointCloud&);
double (*cost)(void*);
};
这是 C++ STL(std::function)内部的高性能做法。
详细内容参考后面的示例。
10. 总结
类型擦除让"不同类型、功能一致"的对象可以在统一容器中操作,而不需继承或模板约束。
C++ 的常用类型擦除做法:
- 定义 Concept(虚接口)
- 定义 Model(实现 Concept,并包含真实对象)
- Wrapper 持有
unique_ptr<Concept>
11.综合示例
下面是一个无虚表(no-virtual),高性能的 type-erasure 实现 (带 Small Buffer Optimization、完整的拷贝/移动/析构语义、可扩展到任意接口的模板化实现)。实现思路:
- 不使用
virtual,用一个静态函数指针表(vtable-like) 保存操作指针(destroy/copy/move/invoke等)。 - 在包装器内部放一个固定大小缓冲区(SBO)来避免小对象的堆分配;大对象落堆分配。
- 通过模板
Model<T>生成具体实现并填充函数指针表。 - 提供常见接口 示例(
setup()/process(PointCloud&)/cost() const),大家可以参考按需扩展或把接口类型做成模板参数。
示例代码(完整、可编译、注释详尽)。建议用 C++17 或更高版本编译(GCC/Clang/MSVC)。
cpp
// high_perf_type_erasure.hpp
#pragma once
#include <cassert>
#include <cstddef>
#include <cstring>
#include <memory>
#include <type_traits>
#include <utility>
#include <iostream>
// Example "PointCloud" placeholder (replace with your actual type)
struct PointCloud {
// dummy
};
// ---------- CONFIG ----------
static constexpr std::size_t SBO_BYTES = 64; // small-buffer size (tune for your workloads)
static constexpr std::size_t SBO_ALIGN = alignof(std::max_align_t);
// ---------- Type-erased Interface (hard-coded example) ----------
// Interface: setup(), process(PointCloud&), cost() const -> double
struct TE_Interface_VTable {
// destroy: destructor for contained object (in-place or heap)
void (*destroy)(void* storage, bool in_sbo) noexcept;
// copy: copy-construct from src storage into dst storage (dst is uninitialized)
void (*copy)(void* dst_storage, bool dst_in_sbo,
const void* src_storage, bool src_in_sbo) ;
// move: move-construct from src into dst, leaving src in valid but unspecified state
void (*move)(void* dst_storage, bool dst_in_sbo,
void* src_storage, bool src_in_sbo) noexcept;
// operations mapping to "concept" methods:
void (*setup)(void* storage) ;
void (*process)(void* storage, PointCloud&) ;
double (*cost)(const void* storage) ;
};
// ---------- Storage layout ----------
struct TE_Storage {
alignas(SBO_ALIGN) unsigned char buf[SBO_BYTES]; // SBO buffer
void* heap_ptr = nullptr; // if object is heap allocated, pointer stored here
bool in_sbo = true; // whether object currently lives in SBO
};
// Helper to get pointer to object (raw)
inline void* TE_storage_ptr(TE_Storage& s) noexcept {
return s.in_sbo ? static_cast<void*>(s.buf) : s.heap_ptr;
}
inline const void* TE_storage_ptr(const TE_Storage& s) noexcept {
return s.in_sbo ? static_cast<const void*>(s.buf) : s.heap_ptr;
}
// ---------- Model generator ----------
template <typename T>
struct TE_Model {
// type properties
static constexpr bool fits_sbo = (sizeof(T) <= SBO_BYTES) &&
(alignof(T) <= SBO_ALIGN) &&
std::is_nothrow_move_constructible<T>::value;
// Destroy
static void destroy(void* storage, bool in_sbo) noexcept {
if (!storage) return;
if (in_sbo) {
T* obj = reinterpret_cast<T*>(storage);
obj->~T();
} else {
T* p = reinterpret_cast<T*>(storage);
delete p;
}
}
// Copy
static void copy(void* dst_storage, bool dst_in_sbo,
const void* src_storage, bool src_in_sbo) {
const T* src = reinterpret_cast<const T*>(src_storage);
if (dst_in_sbo) {
// placement new into dst_storage
new (dst_storage) T(*src);
} else {
// allocate on heap
T* p = new T(*src);
*reinterpret_cast<void**>(dst_storage) = p;
}
}
// Move
static void move(void* dst_storage, bool dst_in_sbo,
void* src_storage, bool src_in_sbo) noexcept {
T* src = reinterpret_cast<T*>(src_storage);
if (dst_in_sbo) {
new (dst_storage) T(std::move(*src));
// destroy source appropriately
if (src_in_sbo) {
src->~T();
} else {
delete src;
}
} else {
// dst on heap
T* p = new T(std::move(*src));
*reinterpret_cast<void**>(dst_storage) = p;
if (src_in_sbo) {
src->~T();
} else {
delete src;
}
}
}
// operations (wrap T's member functions)
static void setup(void* storage) {
T* obj = reinterpret_cast<T*>(storage);
obj->setup();
}
static void process(void* storage, PointCloud& pc) {
T* obj = reinterpret_cast<T*>(storage);
obj->process(pc);
}
static double cost(const void* storage) {
const T* obj = reinterpret_cast<const T*>(storage);
return obj->cost();
}
// Build a vtable instance for T
static TE_Interface_VTable const* vtable() {
static TE_Interface_VTable vt{
&TE_Model<T>::destroy,
&TE_Model<T>::copy,
&TE_Model<T>::move,
&TE_Model<T>::setup,
&TE_Model<T>::process,
&TE_Model<T>::cost
};
return &vt;
}
};
// ---------- The TypeErased wrapper ----------
class TypeErasedModule {
TE_Storage storage_;
TE_Interface_VTable const* vptr_ = nullptr;
// helpers to access dest storage pointer for copy/move routines
static void* dst_construct_ptr(TE_Storage& s) noexcept {
return s.in_sbo ? static_cast<void*>(s.buf) : static_cast<void*>(&s.heap_ptr);
}
static const void* src_construct_ptr(const TE_Storage& s) noexcept {
return s.in_sbo ? static_cast<const void*>(s.buf) : static_cast<const void*>(s.heap_ptr);
}
public:
TypeErasedModule() noexcept = default;
// Construct from concrete T
template <typename T, typename = std::enable_if_t<!std::is_same<std::decay_t<T>, TypeErasedModule>::value>>
TypeErasedModule(T&& obj) {
using U = std::decay_t<T>;
vptr_ = TE_Model<U>::vtable();
constexpr bool fits = TE_Model<U>::fits_sbo;
if (fits) {
storage_.in_sbo = true;
void* dst = static_cast<void*>(storage_.buf);
new (dst) U(std::forward<T>(obj));
} else {
storage_.in_sbo = false;
U* p = new U(std::forward<T>(obj));
storage_.heap_ptr = p;
}
}
// copy ctor
TypeErasedModule(const TypeErasedModule& other) {
vptr_ = other.vptr_;
if (!vptr_) return;
storage_.in_sbo = other.storage_.in_sbo;
if (storage_.in_sbo) {
vptr_->copy(static_cast<void*>(storage_.buf), true,
src_construct_ptr(other.storage_), other.storage_.in_sbo);
} else {
// dst storage is pointer slot (we store pointer into heap_ptr field)
vptr_->copy(static_cast<void*>(&storage_.heap_ptr), false,
src_construct_ptr(other.storage_), other.storage_.in_sbo);
}
}
// move ctor
TypeErasedModule(TypeErasedModule&& other) noexcept {
vptr_ = other.vptr_;
if (!vptr_) return;
storage_.in_sbo = other.storage_.in_sbo;
if (storage_.in_sbo) {
vptr_->move(static_cast<void*>(storage_.buf), true,
const_cast<void*>(src_construct_ptr(other.storage_)), other.storage_.in_sbo);
} else {
vptr_->move(static_cast<void*>(&storage_.heap_ptr), false,
const_cast<void*>(src_construct_ptr(other.storage_)), other.storage_.in_sbo);
}
// leave other empty
other.vptr_ = nullptr;
other.storage_.heap_ptr = nullptr;
other.storage_.in_sbo = true;
}
// copy assign
TypeErasedModule& operator=(const TypeErasedModule& other) {
if (this == &other) return *this;
// destroy current
if (vptr_) vptr_->destroy(TE_storage_ptr(storage_), storage_.in_sbo);
// copy
vptr_ = other.vptr_;
if (!vptr_) {
storage_.heap_ptr = nullptr;
storage_.in_sbo = true;
return *this;
}
storage_.in_sbo = other.storage_.in_sbo;
if (storage_.in_sbo) {
vptr_->copy(static_cast<void*>(storage_.buf), true,
src_construct_ptr(other.storage_), other.storage_.in_sbo);
} else {
vptr_->copy(static_cast<void*>(&storage_.heap_ptr), false,
src_construct_ptr(other.storage_), other.storage_.in_sbo);
}
return *this;
}
// move assign
TypeErasedModule& operator=(TypeErasedModule&& other) noexcept {
if (this == &other) return *this;
if (vptr_) vptr_->destroy(TE_storage_ptr(storage_), storage_.in_sbo);
vptr_ = other.vptr_;
if (!vptr_) {
storage_.heap_ptr = nullptr;
storage_.in_sbo = true;
return *this;
}
storage_.in_sbo = other.storage_.in_sbo;
if (storage_.in_sbo) {
vptr_->move(static_cast<void*>(storage_.buf), true,
const_cast<void*>(src_construct_ptr(other.storage_)), other.storage_.in_sbo);
} else {
vptr_->move(static_cast<void*>(&storage_.heap_ptr), false,
const_cast<void*>(src_construct_ptr(other.storage_)), other.storage_.in_sbo);
}
other.vptr_ = nullptr;
other.storage_.heap_ptr = nullptr;
other.storage_.in_sbo = true;
return *this;
}
// destructor
~TypeErasedModule() {
if (vptr_) {
vptr_->destroy(TE_storage_ptr(storage_), storage_.in_sbo);
}
}
// Concept operations:
void setup() {
assert(vptr_ && "empty TypeErasedModule");
vptr_->setup(TE_storage_ptr(storage_));
}
void process(PointCloud& pc) {
assert(vptr_ && "empty TypeErasedModule");
vptr_->process(TE_storage_ptr(storage_), pc);
}
double cost() const {
assert(vptr_ && "empty TypeErasedModule");
return vptr_->cost(TE_storage_ptr(storage_));
}
// check if empty
explicit operator bool() const noexcept { return vptr_ != nullptr; }
};
使用示例
cpp
#include "high_perf_type_erasure.hpp"
#include <iostream>
// Example concrete modules
struct ICPModule {
void setup() { std::cout << "[ICP] setup\n"; }
void process(PointCloud& pc) { std::cout << "[ICP] process\n"; (void)pc; }
double cost() const { return 0.1; }
};
struct HeavyModule {
// simulate large type
int big_array[100]; // may exceed SBO_BYTES -> heap allocation
HeavyModule() { for (int i=0;i<100;i++) big_array[i]=i; }
void setup() { std::cout << "[Heavy] setup\n"; }
void process(PointCloud& pc) { std::cout << "[Heavy] process\n"; (void)pc; }
double cost() const { return 42.0; }
};
int main(){
PointCloud pc;
TypeErasedModule a = ICPModule{}; // SBO
TypeErasedModule b = HeavyModule{}; // heap
a.setup();
b.setup();
a.process(pc);
b.process(pc);
std::cout << "cost a: " << a.cost() << "\n";
std::cout << "cost b: " << b.cost() << "\n";
// Copy / move tests
TypeErasedModule c = a; // copy
TypeErasedModule d = std::move(b); // move
c.process(pc);
if(d) d.process(pc);
return 0;
}
性能说明与注意事项
-
为何无虚表更快
- 函数-pointer 调用(
vptr_->process(...))实际上仍是 an indirect call,但避免了 per-object vptr pointer to a polymorphic base plus RTTI overhead in some cases; more importantly,这实现让控制 SBO & heap layout tightly and supports no-virtual semantics and allocation avoidance for small objects. - 绝对最高性能情形下可把 vtable指针也内联(模板化)或使用
if constexpr静态分支在编译期消除 indirection(但失去 run-time polymorphism)。
- 函数-pointer 调用(
-
SBO 大小选择
SBO_BYTES应基于目标部署的分配模式(在 SLAM 常用模块多数小于 64B,可将 64 设为默认)。- 若对象大于 SBO,走堆分配,会额外产生
new/delete开销。可配合对象池(object pool)或自定义 allocator 来优化。
-
异常安全
- 在
move/copy中尽量使用noexcept条款(移动要求为 noexcept 才能保证某些容器行为),并在 Model 中对类型要求nothrow_move_constructible来判定是否放进 SBO。若类型移动可能抛异常,需要额外设计(回退/强异常保证)。
- 在
-
对齐
- 使用
alignas保证 SBO 能放下对齐需求较高的类型(这里使用SBO_ALIGN = alignof(std::max_align_t))。若有超对齐类型,需增加SBO_ALIGN。
- 使用
-
线程安全
- 本类型擦除容器本身非线程安全;若并发访问同一对象,需外部同步或把对象做成不可变。
-
接口演变
- 当前实现的接口是固定的(setup/process/cost)。若需要可变接口集合,可以使用 CRTP-style wrapper generator 或把接口定义成一组可选函数指针(按需填充) ------ 代价是更复杂的 vtable 管理。
-
调试
- 在调试信息中会失去具体类型信息(只有函数指针表)。可在
TE_Interface_VTable增加一个const char* type_name字段(typeid(T).name()或用户自定义)以便日志与诊断。
- 在调试信息中会失去具体类型信息(只有函数指针表)。可在