给定一个从
new
表达式获得的指针,用显式调用析构函数(或 delete
)然后调用 std::destroy_at
来替换相应的 operator delete
表达式是否合法?
在我的项目中,我使用了自定义形式的 RTTI。对于具有动态调度析构函数的类型,delete
表达式不会调用适当的析构函数,而是调用基类的析构函数。为了解决这个问题,我“手动”(通过我的 RTTI 机器)根据分配对象的运行时类型调用正确的析构函数,然后直接调用
operator delete
来释放内存。这个解决方案在我的项目中运行良好(用 clang 和 msvc 编译),但我想知道这是否会调用未定义的行为。
关于virtual
:我当然可以通过使它成为
virtual
来实现析构函数的动态调度。然而,这为对象提供了一个 vtable 指针,这在功能上是不需要的,因为我已经在对象中有运行时类型信息。所以
virtual
析构函数解决了这个问题,但是 a) 引入了不必要的运行时开销,更重要的是 b) 它解决了一个已经解决的问题,所以我认为这是一个设计缺陷。
代码示例:
使用C++提供的动态调度机制,问题将由virtual
析构函数解决。
struct Base { virtual ~Base(); };
struct Derived: Base {
// My data members which want to be properly destroyed
};
// ...
Base* p = new Derived(/* ... */);
// ...
delete p; // Derived::~Derived() is being called.
但是由于多种原因,我有一个自定义形式的 RTTI(它解决了 virtual
函数无法解决的问题)。
struct Base { protected: int typeID = ID_OF_TYPE_BASE; };
struct Derived1: Base {
Base() { typeID = ID_OF_TYPE_DERIVED1; }
};
struct Derived2: Base {
Base() { typeID = ID_OF_TYPE_DERIVED2; }
};
给定 p
到
Base
的指针,我可以根据
*p
的实际运行时类型调度函数,类似于
std::visit
上的
std::variant
:
visit(p, [](auto* q) {
// Here *q is statically of the runtime type of *p,
// so either Base, Derived1 or Derived2.
});
这解决了与 virtual
关键字相同的问题(除了类层次结构不易扩展且编写起来不那么漂亮),所以我可以像这样分派到正确的析构函数:
visit(p, [](auto* q) {
std::destroy_at(q);
});
所以我在问以下是否是合法的 C++:
Base* p = new Derived1(/* ... */);
// ...
// delete p; // Calls Base::~Base() - potential memory leak because subobjects of *p are not destroyed.
visit(p, [](auto* q) {
std::destroy_at(q); // Calls Derived1::~Derived1()
::operator delete(q); // Deallocate through pointer to derived type
});
我不是在征求使用定制 RTTI 替代品是否合理的建议。
我也不想暗示使用原始new
和
delete
是很好的做法,这当然隐藏在
unique_ptr
的背后。实际代码:
https://github.com/chrysante/scatha/blob/main/lib/Common/UniquePtr.h
现在,它有效吗?如果你没有做任何不寻常的事情,那可能是,是的。在过去的 15 年左右的时间里,我一直在这样做。我也用内存池做自定义 RTTI,它工作得很好。
只是为了教学,我写了一个例子,在内存池之上使用带有自定义删除的侵入式指针。你可以
在Github上抓取它。
一般来说,我想到的唯一要求是,如果您的类是多态的,并且您要使用指向基类的指针进行删除,则基类必须是虚拟的。