如何通过基类指针自定义释放PMR分配的类层次对象?
我有一个类层次结构,对象存储在std::vector<std::unique_ptr<Base>>中。因为需要频繁对这个向量做增删操作,想通过自定义内存分配避免频繁调用new和delete。我只想用STL工具,所以尝试用std::pmr::unsynchronized_pool_resource做内存分配,同时给std::unique_ptr加自定义删除器。
以下是我目前的实现代码:
#include <memory_resource> #include <vector> #include <memory> // dummy classes struct Base { virtual ~Base() {} }; struct D1 : public Base { D1(int i_) : i(i_) {} int i; }; struct D2 : public Base { D2(double d_) : d(d_) {} double d; }; // custom deleter: this is what I'm concerned about struct Deleter { Deleter(std::pmr::memory_resource& m, std::size_t s, std::size_t a) : mr(m), size(s), align(a) {} void operator()(Base* a) { a->~Base(); mr.get().deallocate(a, size, align); } std::reference_wrapper<std::pmr::memory_resource> mr; std::size_t size, align; }; template <typename T> using Ptr = std::unique_ptr<T, Deleter>; // replacement function for make_unique template <typename T, typename... Args> Ptr<T> newT(std::pmr::memory_resource& m, Args... args) { auto aPtr = m.allocate(sizeof(T), alignof(T)); return Ptr<T>(new (aPtr) T(args...), Deleter(m, sizeof(T), alignof(T))); } // simple construction of vector int main() { auto pool = std::pmr::unsynchronized_pool_resource(); auto vec = std::vector<Ptr<Base>>(); vec.push_back(newT<Base>(pool)); vec.push_back(newT<D1>(pool, 2)); vec.push_back(newT<D2>(pool, 4.0)); return 0; }
这段代码能编译,我觉得不会有内存泄漏(如果有误请指出!),但我对这个需要额外传入大小和对齐参数的Deleter类不太满意。
我最初尝试把它改成模板类,自动获取大小和对齐信息:
template <typename T> struct Deleter { Deleter(std::pmr::memory_resource& m) : mr(m) {} void operator()(Base* a) { a->~Base(); mr.get().deallocate(a, sizeof(T), alignof(T)); } std::reference_wrapper<std::pmr::memory_resource> mr; };
但这样一来,不同类型的std::unique_ptr会不兼容,没法存入同一个向量。
之后我尝试通过基类进行释放:
mr.get().deallocate(a, sizeof(Base), alignof(Base));
这显然是错的,因为释放的内存大小和对齐方式和分配时不一致。
那有没有办法在运行时不存储大小和对齐信息的情况下,通过基类指针完成释放?delete运算符能做到这一点,所以我觉得这里应该也有可行的方法。
解决方案:让基类提供内存释放接口
delete能正确释放派生类对象,本质是虚析构函数隐含了对象的大小、对齐等信息,编译器会为每个派生类生成对应的析构和释放逻辑。我们可以模仿这个思路,让基类提供虚函数,由派生类负责自身的内存释放:
- 修改基类,添加纯虚的
destroy函数:
struct Base { virtual ~Base() {} virtual void destroy(std::pmr::memory_resource& mr) = 0; };
- 每个派生类实现
destroy函数,自行处理析构和内存释放:
struct D1 : public Base { D1(int i_) : i(i_) {} int i; void destroy(std::pmr::memory_resource& mr) override { this->~D1(); mr.deallocate(this, sizeof(D1), alignof(D1)); } }; struct D2 : public Base { D2(double d_) : d(d_) {} double d; void destroy(std::pmr::memory_resource& mr) override { this->~D2(); mr.deallocate(this, sizeof(D2), alignof(D2)); } };
- 简化删除器,不再需要存储大小和对齐信息:
struct Deleter { Deleter(std::pmr::memory_resource& m) : mr(m) {} void operator()(Base* a) { a->destroy(mr.get()); } std::reference_wrapper<std::pmr::memory_resource> mr; };
- 调整
newT函数,无需再传递大小和对齐参数给删除器:
template <typename T, typename... Args> Ptr<T> newT(std::pmr::memory_resource& m, Args... args) { auto aPtr = m.allocate(sizeof(T), alignof(T)); return Ptr<T>(new (aPtr) T(args...), Deleter(m)); }
修改后,删除器不再需要额外存储大小和对齐信息,所有释放逻辑由派生类自行负责,既和delete的工作原理一致,也能保证所有Ptr<Base>类型兼容,可存入同一个向量。
额外优化:用CRTP减少代码冗余
如果不想给每个派生类手动编写destroy函数,可以用CRTP(奇异递归模板模式)简化:
template <typename Derived> struct BaseWithDestroy : Base { void destroy(std::pmr::memory_resource& mr) override { static_cast<Derived*>(this)->~Derived(); mr.deallocate(this, sizeof(Derived), alignof(Derived)); } }; // 派生类继承模板类即可 struct D1 : BaseWithDestroy<D1> { D1(int i_) : i(i_) {} int i; }; struct D2 : BaseWithDestroy<D2> { D2(double d_) : d(d_) {} double d; };
补充说明
你的原始代码确实不会存在内存泄漏,每个对象都正确调用了析构函数,且用分配时的大小和对齐参数调用了deallocate。
内容的提问来源于stack exchange,提问作者Liam Goodacre

