如何缩减类型擦除场景下的二进制体积:避免生成专属删除器
问题背景
在固件开发中实现了无弱指针特性的轻量版std::shared_ptr与std::make_shared,控制块与对象绑定在一起销毁。当前make_shared实现会为每种类型生成专属的lambda作为删除器,导致.text段因大量重复代码占用过多空间。希望优化实现,避免为每种类型生成专属lambda,直接利用析构函数逻辑完成销毁。
当前make_shared实现代码:
template <typename T, typename ... ARGS> shared_ptr<T> make_shared(ARGS&&... args) { struct FullBlock { _shared_ptr_base::control_block block; T value; }; //Allocate block on heap auto block = new FullBlock{ {0, {nullptr, nullptr}}, T{std::forward<ARGS>(args)...} //value }; block->block.destructor.p = block; block->block.destructor.destroy = [](const void* x){ delete static_cast<const FullBlock*>(x); }; return shared_ptr<T>(&block->block, &block->value); }
其中block.destructor.p是指向FullBlock(控制块+对象)的类型擦除指针,block.destructor.destroy是执行销毁的lambda函数指针。用户曾设想直接使用类析构函数指针,但该方式不可行:
struct Foo; using Destructor = void(*)(const void*); Foo* f; Destructor d = &Foo::~Foo; void* ptr = static_cast<void*>(f); d(ptr); // 未定义行为:析构函数签名不匹配
可行优化方案
方案一:利用虚基类实现通用删除器
通过将FullBlock继承自带虚析构的基类,让所有类型的FullBlock共用同一个删除器函数,彻底避免为每种类型生成销毁逻辑:
// 定义带虚析构的基类 struct FullBlockBase { _shared_ptr_base::control_block block; virtual ~FullBlockBase() = default; }; template <typename T, typename ... ARGS> shared_ptr<T> make_shared(ARGS&&... args) { struct FullBlock : FullBlockBase { T value; }; auto block = new FullBlock{ {0, {nullptr, nullptr}}, T{std::forward<ARGS>(args)...} }; block->block.destructor.p = block; // 所有类型共用同一个删除器函数 block->block.destructor.destroy = [](const void* x){ delete static_cast<const FullBlockBase*>(x); }; return shared_ptr<T>(&block->block, &block->value); }
原理:基类的虚析构函数会触发多态调用,自动调用派生类FullBlock<T>的析构函数,进而正确销毁T对象并释放整个FullBlock的内存。此时无论T是什么类型,删除器lambda只会生成一次,大幅减少.text段占用。
方案二:使用模板删除器函数替代lambda
如果不想引入虚函数开销,可以使用模板函数作为删除器,相比lambda,编译器更容易进行代码优化:
template<typename T> void destroy_full_block(const void* ptr) { using FullBlock = struct { _shared_ptr_base::control_block block; T value; }; delete static_cast<const FullBlock*>(ptr); } template <typename T, typename ... ARGS> shared_ptr<T> make_shared(ARGS&&... args) { struct FullBlock { _shared_ptr_base::control_block block; T value; }; auto block = new FullBlock{ {0, {nullptr, nullptr}}, T{std::forward<ARGS>(args)...} }; block->block.destructor.p = block; block->block.destructor.destroy = &destroy_full_block<T>; return shared_ptr<T>(&block->block, &block->value); }
该方案仍会为每种T实例化一次destroy_full_block<T>,但模板函数的代码生成通常比lambda更紧凑,且避免了lambda捕获带来的额外隐式逻辑。
为什么直接使用析构函数指针不可行
类的非虚析构函数签名实际为void(T*)(隐式接收this指针),而用户定义的Destructor类型是void(*)(const void*),直接强转函数指针会导致调用时的指针类型不匹配,属于未定义行为,可能引发内存泄漏、程序崩溃等问题。即使是虚析构函数,其底层实现也依赖于虚表,无法直接通过void(*)(const void*)类型的指针正确调用。
内容的提问来源于stack exchange,提问作者Patrick Wright

