类继承与重载operator delete的对象删除UB问题原因及修复咨询
对象延迟删除机制的未定义行为分析与修复
问题描述
尝试实现对象延迟删除机制时出现未定义行为(UB),不同编译器表现差异明显:部分环境出现段错误,MSVC下AddressSanitizer提示类型不匹配的释放错误。核心代码如下:
#include <iostream> struct Base { Base() { std::cout << "Base::ctor\n"; } virtual ~Base() { std::cout << "Base::dtor\n"; } virtual void invoke() = 0; }; template <typename T> class PtrKeeper { public: PtrKeeper() { std::cout << "PtrKeeper::ctor\n"; } virtual ~PtrKeeper() { std::cout << "PtrKeeper::dtor\n"; } void operator delete(void* p) { std::cout << "PtrKeeper::operator delete\n"; destroy(p); } static void destroy(void* p) { delete static_cast<T*>(p); } }; struct Derived : public Base , public PtrKeeper<Derived> { Derived() { std::cout << "Derived::ctor\n"; } virtual ~Derived() { std::cout << "Derived::dtor\n"; } virtual void invoke() { delete this; } }; int main() { auto obj = new Derived(); obj->invoke(); return 0; }
不同编译器表现
- GCC环境输出:
Base::ctor PtrKeeper::ctor Derived::ctor Derived::dtor PtrKeeper::dtor Base::dtor PtrKeeper::operator delete
- Clang环境输出:
Base::ctor PtrKeeper::ctor Derived::ctor Derived::dtor PtrKeeper::dtor Base::dtor PtrKeeper::operator delete Segmentation fault (core dumped)
- MSVC 2022输出:
Base::ctor PtrKeeper::ctor Derived::ctor Derived::dtor PtrKeeper::dtor Base::dtor PtrKeeper::operator delete Base::dtor
- MSVC AddressSanitizer提示:
==21628==ERROR: AddressSanitizer: new-delete-type-mismatch on 0x114eb19a50d0 in thread T3: object passed to delete has wrong type: size of the allocated type: 48 bytes; size of the deallocated type: 24 bytes.
未定义行为原因
双重删除:
执行delete this时,流程如下:- 首先调用
Derived的析构函数,依次触发~Derived()、~PtrKeeper()、~Base(),完成对象析构。 - 随后调用
Derived继承自PtrKeeper<Derived>的operator delete,该函数内部又调用delete static_cast<T*>(p),对已析构释放的对象再次执行delete,导致双重释放内存,触发UB。
- 首先调用
指针类型与偏移错误:
Derived是多继承类,PtrKeeper<Derived>作为第二个基类,其子对象地址与Derived对象起始地址存在偏移。PtrKeeper::operator delete的参数p若未被正确调整偏移,static_cast<T*>(p)会生成错误指针,访问不属于该对象的内存,这也是ASAN提示类型大小不匹配的原因。
修复方案(保留原设计思路)
核心目标是避免双重删除,确保指针操作正确性,以下是两种可行方案:
方案1:修正PtrKeeper::operator delete实现
将operator delete改为直接调用全局内存释放函数,不再重复删除对象(析构已完成):
template <typename T> class PtrKeeper { public: // 其他成员不变 void operator delete(void* p) { std::cout << "PtrKeeper::operator delete\n"; // 调用全局operator delete释放内存,避免重复删除 ::operator delete(p); } // 调整destroy方法,避免触发重复删除 static void destroy(void* p) { // 若需保留destroy,可改为手动析构+释放(需确保仅调用一次) // T* obj = static_cast<T*>(p); // obj->~T(); // ::operator delete(obj); } };
方案2:调整调用逻辑,由PtrKeeper统一管理销毁
修改Derived::invoke直接调用PtrKeeper的销毁方法,让destroy手动完成析构和内存释放,避免依赖默认delete流程:
template <typename T> class PtrKeeper { public: // 其他成员不变 // 移除自定义operator delete,避免与默认流程冲突 static void destroy(void* p) { T* obj = static_cast<T*>(p); // 手动调用析构函数 obj->~T(); // 调用全局operator delete释放内存 ::operator delete(obj); } }; struct Derived : public Base , public PtrKeeper<Derived> { // 其他成员不变 virtual void invoke() { // 直接调用PtrKeeper的destroy方法,而非delete this PtrKeeper<Derived>::destroy(this); } };
两种方案均可避免双重删除和指针偏移问题,同时保留PtrKeeper管理对象销毁的设计思路。
内容的提问来源于stack exchange,提问作者Aleksey Ivanov
相关产品推荐
相关产品推荐

