ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

类型擦除:手写 any 与 function 的原理

类型擦除:手写 any 与 function 的原理 std::any能装任意类型std::function能包任意可调用对象。这俩能力差得远底层却是同一招类型擦除type erasure用「非模板接口 模板派生的壳 虚函数跳板」把具体类型藏起来。这篇把两个都手写一遍真跑看完你就明白标准库这两个类在做什么、以及为什么它能省掉模板的代码膨胀。1. 引子一个矛盾的需求想写一个「能装任何类型」的容器或者「能包任何 lambda」的回调会遇到同一个矛盾如果用模板template typename T class Box类型在编译期确定每个类型生成一份代码 —— 类型一多二进制就膨胀而且无法放进同一个vector里统一处理如果用继承多态Base*指向Derived能统一放进容器但所有类型都必须提前继承自同一个Base—— 你没法让int去继承一个基类。类型擦除是第三条路对外只暴露一个非模板的「接口」类内部用模板把任意类型包装成一个实现了该接口的「壳」通过虚函数跳板访问真实数据。它既统一了接口能放进vectorAny又不要求类型有任何继承关系。2. 手写 anytypeid 校验取值std::any的骨架就三步一个抽象基类Base接口一个模板派生类HolderT壳一个unique_ptrBase成员持有壳的指针。取值时用typeid校验「你想要的类型」和「我存的类型」是不是同一个// any_demo.cpp — 编译: g -stdc17 -Wall -O2 any_demo.cpp -o anydemo #include cstdio #include memory #include string #include typeinfo #include utility class Any { struct Base { // 非模板接口把所有类型统一成它 virtual ~Base() default; virtual const std::type_info type() const 0; virtual std::unique_ptrBase clone() const 0; }; template typename T struct Holder : Base { // 模板壳真正装数据的地方 T value; explicit Holder(T v) : value(std::move(v)) {} const std::type_info type() const override { return typeid(T); } std::unique_ptrBase clone() const override { return std::make_uniqueHolderT(value); } }; std::unique_ptrBase ptr_; public: Any() default; Any(const Any other) : ptr_(other.ptr_ ? other.ptr_-clone() : nullptr) {} Any(Any) noexcept default; Any operator(const Any other) { ptr_ other.ptr_ ? other.ptr_-clone() : nullptr; return *this; } Any operator(Any) noexcept default; template typename T Any(T v) : ptr_(std::make_uniqueHolderT(std::move(v))) {} // 隐式任何类型都能塞进来 bool has_value() const { return ptr_ ! nullptr; } const std::type_info type() const { return ptr_ ? ptr_-type() : typeid(void); } template typename T T cast() { // 取值先 typeid 校验再 static_cast if (!ptr_ || ptr_-type() ! typeid(T)) throw std::bad_cast(); return static_castHolderT*(ptr_.get())-value; } }; int main() { Any a 42; std::printf(a 是 int %d值 %d\n, static_castint(a.type() typeid(int)), a.castint()); Any b std::string(hello); std::printf(b 是 string %d值 %s\n, static_castint(b.type() typeid(std::string)), b.caststd::string().c_str()); bool threw false; try { a.caststd::string(); } // 把 int 当 string 取 → 抛异常 catch (const std::bad_cast) { threw true; } std::printf(取错类型抛异常 %d\n, static_castint(threw)); }a 是 int 1值 42 b 是 string 1值 hello 取错类型抛异常 1内存布局是这样的概念图手写 Any 的内存布局类型擦掉藏进虚表 Any 对象栈上约 8 字节 ┌──────────────────┐ │ unique_ptrBase ┼──► 堆上的 HolderT └──────────────────┘ ┌──────────────────────────────┐ │ vptr ──► Base 的虚函数表 │ ← 具体类型 T 被擦掉 │ type() 返回 typeid(T) │ 藏进虚表指针 │ clone() 复制一个新的 HolderT │ │ value : T真正的数据 │ └──────────────────────────────┘ castT()先比 type()typeid(T)对上再 static_cast 到 HolderT 拿 valuecastT()里那句static_castHolderT*之所以安全正是因为前面已经用typeid(T)校验过「存的确实是 T」—— 类型擦除不是「丢掉类型信息」而是「把类型信息藏进虚表取用时再核对」。这也解释了为什么any取错类型是运行时才报抛bad_cast而模板是编译期就报。官方文档std::any · std::type_infotypeid返回的就是它3. 手写 functioninvoker 跳板std::function的套路一模一样只是把「存数据」换成「存一个可调用对象」虚函数从type()换成call()。这个负责调用的虚函数就是所谓的invoker跳板// function_demo.cpp — 编译: g -stdc17 -Wall -O2 function_demo.cpp -o fndemo #include cstdio #include memory #include type_traits #include utility template typename Signature class Function; // 主模板只声明不定义 template typename R, typename... Args class FunctionR(Args...) { struct InvokerBase { // 非模板接口统一的「可调用」抽象 virtual ~InvokerBase() default; virtual R call(Args... args) 0; virtual std::unique_ptrInvokerBase clone() const 0; }; template typename F struct Invoker : InvokerBase { // 模板壳包住任意可调用对象 F f; explicit Invoker(F f) : f(std::move(f)) {} R call(Args... args) override { // 跳板把虚调用转发给 f if constexpr (std::is_void_vR) { f(args...); } else { return f(args...); } } std::unique_ptrInvokerBase clone() const override { return std::make_uniqueInvokerF(f); } }; std::unique_ptrInvokerBase inv_; public: Function() default; Function(const Function other) : inv_(other.inv_ ? other.inv_-clone() : nullptr) {} Function(Function) noexcept default; Function operator(const Function other) { inv_ other.inv_ ? other.inv_-clone() : nullptr; return *this; } Function operator(Function) noexcept default; template typename F Function(F f) : inv_(std::make_uniqueInvokerF(std::move(f))) {} // 任何可调用对象都能包 R operator()(Args... args) { return inv_-call(args...); } // 对外像普通函数一样调 }; int add(int x, int y) { return x y; } int main() { Functionint(int, int) f1 add; // 包函数指针 Functionint(int, int) f2 [](int x, int y) { return x * y; }; // 包 lambda Functionvoid(int) printer [](int v) { std::printf(value %d\n, v); }; // 包 void 返回 std::printf(add(3,4) %d\n, f1(3, 4)); std::printf(mul(3,4) %d\n, f2(3, 4)); printer(7); }add(3,4) 7 mul(3,4) 12 value 7调用链长这样手写 Function 的调用链虚调用只发生在跳板这一层 FunctionR(Args...) 对象 ┌───────────────────────────┐ │ unique_ptrInvokerBase ┼──► InvokerF └───────────────────────────┘ │ f : Flambda / 函数指针 │ call(Args...) ← 唯一的虚调用跳板 └──────┬─────── fn(1,2) ─► operator() ─► inv_-call(1,2) ─► f(1,2) 非模板代码 虚函数间接调用 模板实例每种 F 一份可内联这里的关键分工Function类和InvokerBase都是非模板的编译成一份只有InvokerF这个壳是模板每个可调用对象类型一份但很小只做转发。于是vectorFunctionint(int,int)可以装下一堆签名相同、实现各异的 lambda而不用为它们各自写一份完整的包装代码。if constexpr (std::is_void_vR)处理了「返回void」的特例void函数不能return f(...)只能「调用但不返回」。这也是std::function内部会用到的技巧。官方文档std::function · std::make_unique4. 为什么这能省模板代码膨胀把三种「统一不同类型」的写法放一起比方案类型何时确定代码份数运行时开销适合场景模板templateclass T编译期每种类型一份无可内联类型已知、追求零开销继承多态Base*运行时基类接口一份一次虚调用类型都继承自同一基类类型擦除any/function运行时一份接口 每类型一个小壳一次虚调用 一次堆分配类型未知、但要统一接口模板的「代码膨胀」来自每个实例化都会把整个函数体复制一份。类型擦除的聪明之处在于它把「必须随类型变化」的那部分压进一个极小的模板壳HolderT/InvokerF而「不变的大头」接口、管理逻辑编译成一份非模板代码。代价是每次访问多一次虚调用、对象多一次堆分配。所以类型擦除不是「免费午餐」它是在「灵活」和「零开销」之间取了个平衡。5. 完整示例异构容器 可调用对象表把 any 和 function 串起来一个vectorAny装异构数据按类型分派一个vectorFunctionint(int,int)装同签名不同实现的算子// erasure_full.cpp — 编译: g -stdc17 -Wall -O2 erasure_full.cpp -o efull #include cstdio #include memory #include string #include type_traits #include typeinfo #include utility #include vector class Any { struct Base { virtual ~Base() default; virtual const std::type_info type() const 0; virtual std::unique_ptrBase clone() const 0; }; template typename T struct Holder : Base { T value; explicit Holder(T v) : value(std::move(v)) {} const std::type_info type() const override { return typeid(T); } std::unique_ptrBase clone() const override { return std::make_uniqueHolderT(value); } }; std::unique_ptrBase ptr_; public: Any() default; Any(const Any other) : ptr_(other.ptr_ ? other.ptr_-clone() : nullptr) {} Any(Any) noexcept default; template typename T Any(T v) : ptr_(std::make_uniqueHolderT(std::move(v))) {} const std::type_info type() const { return ptr_ ? ptr_-type() : typeid(void); } template typename T T cast() { if (!ptr_ || ptr_-type() ! typeid(T)) throw std::bad_cast(); return static_castHolderT*(ptr_.get())-value; } }; template typename Signature class Function; template typename R, typename... Args class FunctionR(Args...) { struct InvokerBase { virtual ~InvokerBase() default; virtual R call(Args... args) 0; virtual std::unique_ptrInvokerBase clone() const 0; }; template typename F struct Invoker : InvokerBase { F f; explicit Invoker(F f) : f(std::move(f)) {} R call(Args... args) override { if constexpr (std::is_void_vR) { f(args...); } else { return f(args...); } } std::unique_ptrInvokerBase clone() const override { return std::make_uniqueInvokerF(f); } }; std::unique_ptrInvokerBase inv_; public: Function() default; Function(const Function other) : inv_(other.inv_ ? other.inv_-clone() : nullptr) {} Function(Function) noexcept default; template typename F Function(F f) : inv_(std::make_uniqueInvokerF(std::move(f))) {} R operator()(Args... args) { return inv_-call(args...); } }; int main() { std::vectorAny items; // 异构数据同一个容器装三种类型 items.emplace_back(42); items.emplace_back(std::string(hello)); items.emplace_back(3.14); for (auto item : items) { if (item.type() typeid(int)) { std::printf(int %d\n, item.castint()); } else if (item.type() typeid(std::string)) { std::printf(string %s\n, item.caststd::string().c_str()); } else if (item.type() typeid(double)) { std::printf(double %.2f\n, item.castdouble()); } } std::vectorFunctionint(int, int) ops; // 同签名不同实现一个容器装三种算法 ops.emplace_back([](int a, int b) { return a b; }); ops.emplace_back([](int a, int b) { return a - b; }); ops.emplace_back([](int a, int b) { return a * b; }); for (auto op : ops) { std::printf(op(6,3) %d\n, op(6, 3)); } }int 42 string hello double 3.14 op(6,3) 9 op(6,3) 3 op(6,3) 18同一个vectorAny装下了int、std::string、double三种毫无继承关系的类型靠typeid分派同一个vectorFunctionint(int,int)装下了三个不同的 lambda靠 invoker 跳板统一调用。这正是类型擦除的价值把「类型未知」的灵活性和「统一接口」的工程性同时拿到了手。代价只是每次访问多一次虚调用、一次堆分配。6. 延伸阅读std::any — cppreference —— 类型擦除容器的标准实现这篇手写Any的对照std::function — cppreference —— 可调用对象包装器这篇手写Function的对照Type erasure类型擦除— cppreference —— 这一机制的语言层面说明std::type_info — cppreference ——typeid返回的类型any取值校验靠它Compiler Explorer — godbolt.org —— 把手写Function贴进去看operator()这条链路上虚调用和模板实例各编译成了什么本知识库内的相关篇目《类型列表与编译期算法手写 typelist 的 map/filter》 —— 编译期想对「一组类型」做运算取第 N 个、映射成指针、筛选整型《表达式模板Eigen 式延迟求值把 abc 压成一次遍历》 —— 表达式模板Expression Templates让 abc 不再生成中间临时对象《tag dispatch 与重载偏序实战按迭代器类别派发算法》 —— tag dispatch标签派发用「空类型」当标签7. 一句话总结类型擦除 非模板接口Base/InvokerBase 模板派生壳HolderT/InvokerF 虚函数跳板type()/call()把具体类型藏进虚表、取用时再核对 ——any用typeid校验后static_cast取值、取错抛bad_castfunction用 invoker 虚调用把operator()转发给任意 lambda这套机制把「必须随类型变化」的部分压进极小模板壳、不变的大头编译成一份从而在「类型未知」和「代码不膨胀」之间取到平衡代价是每次访问一次虚调用加一次堆分配。
返回列表