C++ 类型擦除详解

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

基于 C++11+,重点 C++17/20 改进。直奔主题:概念、原理、标准实现、手动示例、优缺点。

1. 类型擦除是什么?为什么用?

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

痛点解决

  • 模板太静态,无法动态类型(如插件、网络数据)。
  • 容器存异构类型:用 Any 包装。
  • API 解耦:接受任意 callable,不限 lambda/functor。

不是语法,是模式:动态多态 + vtable。

2. 实现原理

三件套

  • 接口:纯虚基类(clone, destroy, type)。
  • Holder:模板派生,持值,实现虚函数。
  • 包装器:持 Base*,转发调用。

流程:

  1. T val → 创建 Holder<T>
  2. unique_ptr<Base> 指向它 → 类型擦除。
  3. 调用 → vtable 分发。

优化:小对象优化(SBO),栈存小 T,避免堆。

vs 模板:运行时开销(虚调用、heap),但灵活。
vs variant:variant 静态知所有类型,无 heap。

3. 标准库示例

std::any (C++17)

存任意 T,用 any_cast<T*> 取出。

简化伪实现(C++11,纯堆分配示意,核心 vtable 类型擦除):

#include <memory>
#include <typeinfo>
#include <type_traits>
#include <stdexcept>
#include <utility>

struct AnyVTable {
  void* (*clone)(const void* src);
  void (*destroy)(void* ptr);
  const std::type_info& (*type)();
};

template<typename T>
const AnyVTable* get_vtable() {
  static const AnyVTable vt {
    [](const void* src) -> void* {
      return new T(*static_cast<const T*>(src));
    },
    [](void* ptr) {
      static_cast<T*>(ptr)->~T();
      delete static_cast<T*>(ptr);
    },
    []() -> const std::type_info& { return typeid(T); }
  };
  return &vt;
}

class Any {
private:
  void* ptr_;
  const AnyVTable* vtable_;

public:
  template<typename Value>
  Any(Value&& val)
    : ptr_(new std::decay_t<Value>(std::forward<Value>(val)))
    , vtable_(get_vtable<std::decay_t<Value>>()) {}

  Any(const Any& other)
    : vtable_(other.vtable_)
    , ptr_(other.vtable_->clone(other.ptr_)) {}

  ~Any() {
    vtable_->destroy(ptr_);
  }

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

std::function (C++11)

统一 callable。

伪实现类似:vtable 有 invoke

用时:std::function<int(int)> f = [](int x){return x*x;};

4. 手动实现 Any

完整 C++11 Any,支持拷贝/移动/类型检查。

#include <memory>
#include <typeinfo>
#include <stdexcept>
#include <utility>
#include <iostream>
#include <string>

class Any {
public:
  template<typename T>
  Any(T&& value) : ptr_(make_holder(std::forward<T>(value))) {}

  Any(const Any& other) : ptr_(other.ptr_ ? other.ptr_->clone() : nullptr) {}
  Any(Any&& other) noexcept : ptr_(std::move(other.ptr_)) {}

  Any& operator=(const Any& other) {
    if (this != &other) {
      ptr_ = other.ptr_ ? other.ptr_->clone() : nullptr;
    }
    return *this;
  }

  Any& operator=(Any&& other) noexcept {
    ptr_ = std::move(other.ptr_);
    return *this;
  }

  ~Any() = default;

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

  template<typename T>
  const T& get() const& {
    auto* holder = dynamic_cast<Holder<T>*>(ptr_.get());
    if (!holder) throw std::bad_any_cast{};
    return holder->value_;
  }

  bool empty() const { return !ptr_; }

private:
  struct Concept {
    virtual ~Concept() = default;
    virtual Concept* clone() const = 0;
  };

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

  std::unique_ptr<Concept> ptr_;
};

测试

int main() {
  Any a = 42;
  Any b = std::string("hello");

  std::cout << a.get<int>() << '\n';           // 42
  std::cout << b.get<std::string>() << '\n';   // hello

  return 0;
}

关键dynamic_cast 类型安全。拷贝用 clone()

5. 优缺点

优点

  • 运行时灵活:插件、事件。
  • 统一接口,少模板代码。

缺点

  • 性能烂:虚调用 + heap(用 variant 替代)。
  • 调试难:类型隐藏。
  • 大小大:多 pointer。

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

6. 扩展

  • Boost.TypeErasure:高级。
  • C++20 concepts:约束模板擦除。

用标准库,别重造。除非特殊需求。