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

类型擦除是什么?

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

解决痛点

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

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

实现原理

三件套

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

vs 模板:运行时开销(虚调用、heap),但灵活。
vs variant:variant 是静态类型擦除,编译期已知所有类型,无 heap 分配。

标准库示例

std::any (C++17)

简化实现(核心:vtable 类型擦除):

// 简化版 std::any:使用 vtable 实现类型擦除
class Any {
    // VTable: 存储类型相关的操作函数指针
    struct VTable {
        void* (*clone)(const void*);           // 拷贝函数:深拷贝对象
        void (*destroy)(void*);                // 析构函数:释放对象
        const std::type_info& (*type)();       // 类型信息函数:获取类型ID
    };
    void* ptr_;               // 指向堆上存储的实际对象
    const VTable* vt_;        // 指向类型对应的 vtable

public:
    // 构造函数:接受任意类型,完美转发到堆上
    template<typename T>
    Any(T&& val) : ptr_(new T(std::forward<T>(val))),
                   vt_(get_vtable<T>()) {}
    
    // 类型安全的转换:运行时检查类型是否匹配
    template<typename T>
    T* cast() {
        if (vt_->type() != typeid(T)) throw std::bad_any_cast{};
        return static_cast<T*>(ptr_);
    }
    
    // 析构:通过 vtable 调用正确的析构函数
    ~Any() { vt_->destroy(ptr_); }
};

std::function (C++11)

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

核心思想

问题:如何用一个类型存储任意 callable(lambda、函数指针、函数对象)?

答案:类型擦除 + 虚函数表(vtable)

┌─────────────────────────────────────────────────────────────┐
│  std::function<int(int)> f                                   │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  vtable*  ──→ [destroy][clone][call]                │    │
│  │  storage  ──→ 实际的 callable(类型已擦除)          │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

关键概念

  1. vtable(虚函数表):存储类型相关的操作函数指针

    • 每种 callable 类型对应一个 vtable
    • 编译期生成,运行时查找
  2. SBO(Small Buffer Optimization):小对象优化

    • 小对象(≤32字节)直接存在栈上,避免堆分配
    • 大对象才分配到堆上
  3. 类型擦除:通过 void* 或 char 数组存储,忘记具体类型

完整实现(带详细注释)

// 简化版 std::function:类型擦除 + 小对象优化(SBO)
template<typename Ret, typename... Args>
class Function {
    // === 第一步:定义 vtable(虚函数表) ===
    // vtable 存储类型相关的操作函数指针,每种 callable 类型对应一个 vtable
    struct VTable {
        void (*destroy)(void*);                // 析构函数:释放 callable
        void (*clone)(const void*, void*);     // 拷贝函数:复制 callable
        Ret (*call)(void*, Args...);           // 调用函数:执行 callable
    };
    
    // === 第二步:定义存储空间 ===
    // SBO: Small Buffer Optimization - 小对象优化
    // 小对象(≤32字节)直接存储在栈上,避免堆分配开销
    alignas(16) char buf_[32];  // 对齐到16字节,满足大多数类型的对齐要求
    void* ptr_ = nullptr;       // 大对象时使用:指向堆上存储的 callable
    const VTable* vt_ = nullptr; // 指向类型对应的 vtable

    // 编译期判断:类型F是否可以用SBO
    template<typename F>
    static constexpr bool CanSBO = sizeof(F) <= 32;

    // === 第三步:为每种 callable 类型生成对应的 vtable ===
    template<typename F>
    static const VTable* get_vtable() {
        static const VTable vt = {
            // destroy: 调用 F 的析构函数
            [](void* p) { static_cast<F*>(p)->~F(); },
            
            // clone: 拷贝构造 F
            [](const void* src, void* dst) {
                new(dst) F(*static_cast<const F*>(src));
            },
            
            // call: 调用 F 的 operator()
            [](void* p, Args... args) -> Ret {
                return (*static_cast<F*>(p))(std::forward<Args>(args)...);
            }
        };
        return &vt;
    }

public:
    // === 第四步:构造函数 - 接受任意 callable ===
    template<typename F>
    Function(F&& f) {
        vt_ = get_vtable<F>();  // 获取类型对应的 vtable
        
        if constexpr (CanSBO<F>) {
            // SBO: 小对象直接在 buf_ 上构造,placement new
            // 避免堆分配,提升性能
            new(buf_) F(std::forward<F>(f));
        } else {
            // 大对象:分配到堆上
            ptr_ = new F(std::forward<F>(f));
        }
    }
    
    // === 第五步:调用操作符 - 通过 vtable 转发 ===
    Ret operator()(Args... args) {
        // ptr_ 非空则用 ptr_(堆上),否则用 buf_(栈上)
        void* storage = ptr_ ? ptr_ : buf_;
        return vt_->call(storage, std::forward<Args>(args)...);
    }
    
    // === 第六步:析构 - 根据存储位置调用正确的析构 ===
    ~Function() {
        void* storage = ptr_ ? ptr_ : buf_;
        vt_->destroy(storage);
        
        // 如果是堆分配,需要释放内存
        if (ptr_) {
            delete static_cast<char*>(ptr_);
        }
    }
};

工作流程图解

构造阶段:
┌─────────────────────────────────────────────────────────────┐
│  Function<int(int)> f = [](int x) { return x * x; };        │
│                                                             │
│  1. lambda 类型推断 → LambdaType                            │
│  2. get_vtable<LambdaType>() → 生成 vtable                  │
│     ┌─────────────────────────────────────┐                 │
│     │ destroy: LambdaType::~LambdaType   │                 │
│     │ clone:   LambdaType 的拷贝构造      │                 │
│     │ call:    LambdaType::operator()    │                 │
│     └─────────────────────────────────────┘                 │
│  3. sizeof(LambdaType) ≤ 32? → 是 → SBO                     │
│  4. new(buf_) LambdaType(...) → 存储在栈上                   │
└─────────────────────────────────────────────────────────────┘

调用阶段:
┌─────────────────────────────────────────────────────────────┐
│  f(5)                                                       │
│                                                             │
│  1. 判断存储位置 → buf_(SBO)                               │
│  2. vt_->call(buf_, 5) → 虚函数调用                         │
│  3. 执行 lambda → return 25                                 │
└─────────────────────────────────────────────────────────────┘

使用示例

// 示例1:存储 lambda
Function<int(int)> f = [](int x) { return x * x; };
std::cout << f(5);  // 输出: 25

// 示例2:存储函数指针
int add(int a, int b) { return a + b; }
Function<int(int, int)> g = add;
std::cout << g(3, 4);  // 输出: 7

// 示例3:存储 std::bind 结果
Function<void()> h = std::bind([](int a, int b) { 
    std::cout << a + b << std::endl; 
}, 3, 4);
h();  // 输出: 7

// 示例4:存储带捕获的 lambda(SBO)
int multiplier = 10;
Function<int(int)> square = [multiplier](int x) { 
    return x * x * multiplier; 
};
std::cout << square(3);  // 输出: 90

// 示例5:存储大对象(非 SBO,堆分配)
struct BigCallable {
    std::array<int, 100> data_;  // 400 字节,超过 SBO 阈值
    int operator()(int x) { return data_[0] + x; }
};
Function<int(int)> big = BigCallable{};

性能对比

场景SBO(小对象)非SBO(大对象)
构造栈上 placement new堆分配 + 构造
调用虚函数调用虚函数调用
析构直接调用析构析构 + 堆释放
内存开销32字节栈空间堆内存 + 指针

关键设计总结

  1. vtable:编译期生成,运行时查找,实现类型擦除
  2. SBO:小对象优化,避免堆分配,提升性能
  3. 存储策略CanSBO<T> 编译期判断,自动选择最优存储方式

手动实现 Any(经典模式)

// 手动实现 Any:经典类型擦除模式(虚函数版)
class Any {
    // === 1. 接口层:纯虚基类,定义统一操作接口 ===
    struct Concept {
        virtual ~Concept() = default;
        virtual Concept* clone() const = 0;  // 虚拷贝,支持 Any 的拷贝构造
    };
    
    // === 2. Holder 层:模板派生类,持有具体类型 ===
    template<typename T>
    struct Holder : Concept {
        T val_;  // 实际存储的值
        
        // 构造函数:完美转发初始化值
        Holder(T&& v) : val_(std::forward<T>(v)) {}
        
        // 虚拷贝实现:创建新的 Holder 副本
        Concept* clone() const override { 
            return new Holder(T{val_});  // 拷贝构造 T
        }
    };
    
    // === 3. 包装器层:管理 Concept 指针,对外提供统一接口 ===
    std::unique_ptr<Concept> ptr_;  // 用智能指针管理生命周期

public:
    // 万能构造函数:接受任意类型 T
    template<typename T> 
    Any(T&& v) : ptr_(new Holder<T>(std::forward<T>(v))) {}
    
    // 获取存储的值:运行时类型检查
    template<typename T>
    T& get() {
        // dynamic_cast 进行安全的向下转型
        auto* h = dynamic_cast<Holder<T>*>(ptr_.get());
        if (!h) throw std::bad_cast{};  // 类型不匹配
        return h->val_;  // 返回实际值的引用
    }
};

// ===== 使用示例 =====
Any a = 42;                          // 存储 int
Any b = std::string("hello");        // 存储 string
Any c = a;                           // 拷贝构造(通过虚 clone())

std::cout << a.get<int>();           // 输出: 42
std::cout << b.get<std::string>();   // 输出: hello
// std::cout << a.get<std::string>(); // 运行时抛出 std::bad_cast

优缺点

优点缺点
运行时灵活(插件、事件)性能损失:虚调用 + heap
统一接口,少模板代码调试难:类型隐藏
内存占用大

何时用:真有动态需求。优先用模板/variant。

总结

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