C++包装器之类型擦除(Type Erasure)包装器详解(4)


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::function
  • std::any
  • LLVM 的 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++ 的常用类型擦除做法:

  1. 定义 Concept(虚接口)
  2. 定义 Model(实现 Concept,并包含真实对象)
  3. 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;
}

性能说明与注意事项

  1. 为何无虚表更快

    • 函数-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)。
  2. SBO 大小选择

    • SBO_BYTES 应基于目标部署的分配模式(在 SLAM 常用模块多数小于 64B,可将 64 设为默认)。
    • 若对象大于 SBO,走堆分配,会额外产生 new/delete 开销。可配合对象池(object pool)或自定义 allocator 来优化。
  3. 异常安全

    • move / copy 中尽量使用 noexcept 条款(移动要求为 noexcept 才能保证某些容器行为),并在 Model 中对类型要求 nothrow_move_constructible 来判定是否放进 SBO。若类型移动可能抛异常,需要额外设计(回退/强异常保证)。
  4. 对齐

    • 使用 alignas 保证 SBO 能放下对齐需求较高的类型(这里使用 SBO_ALIGN = alignof(std::max_align_t))。若有超对齐类型,需增加 SBO_ALIGN
  5. 线程安全

    • 本类型擦除容器本身非线程安全;若并发访问同一对象,需外部同步或把对象做成不可变。
  6. 接口演变

    • 当前实现的接口是固定的(setup/process/cost)。若需要可变接口集合,可以使用 CRTP-style wrapper generator 或把接口定义成一组可选函数指针(按需填充) ------ 代价是更复杂的 vtable 管理。
  7. 调试

    • 在调试信息中会失去具体类型信息(只有函数指针表)。可在 TE_Interface_VTable 增加一个 const char* type_name 字段(typeid(T).name() 或用户自定义)以便日志与诊断。

相关推荐
小尧嵌入式1 小时前
C++中的封装继承多态
开发语言·arm开发·c++
yugi9878381 小时前
TDOA算法MATLAB实现:到达时间差定位
前端·算法·matlab
t198751281 小时前
基于因子图与和积算法的MATLAB实现
开发语言·算法·matlab
le serein —f1 小时前
用go实现-回文链表
算法·leetcode·golang
rit84324991 小时前
MFOCUSS算法MATLAB实现:稀疏信号重构
算法·matlab·重构
发疯幼稚鬼1 小时前
散列及其分离链接法
c语言·数据结构·算法·链表·散列表
Bdygsl1 小时前
数字图像处理总结 Day 1
人工智能·算法·计算机视觉
北郭guo1 小时前
垃圾回收底层原理【深入了解】
java·jvm·算法
小年糕是糕手1 小时前
【C++同步练习】C++入门
开发语言·数据结构·c++·算法·pdf·github·排序算法