Prior to C++20, objects’ destructors were always called prior to calling their operator delete. With destroying operator delete in C++20, operator delete can instead call the destructor itself. Here’s a very simple toy example of non-destroying vs. destroying operator delete:
#include <iostream>
#include <new>
struct Foo {
~Foo() {
std::cout << "In Foo::~Foo()\n";
}
void operator delete(void *p) {
std::cout << "In Foo::operator delete(void *)\n";
::operator delete(p);
}
};
struct Bar {
~Bar() {
std::cout << "In Bar::~Bar()\n";
}
void operator delete(Bar *p, std::destroying_delete_t) {
std::cout << "In Bar::operator delete(Bar *, std::destroying_delete_t)\n";
p->~Bar();
::operator delete(p);
}
};
int main() {
delete new Foo;
delete new Bar;
}
And the output:
In Foo::~Foo()
In Foo::operator delete(void *)
In Bar::operator delete(Bar *, std::destroying_delete_t)
In Bar::~Bar()
Key facts about it:
- A destroying
operator deletefunction must be a class member function. - If more than one
operator deleteis available, a destroying one will always take precedence over a non-destroying one. - The difference between the signatures of non-destroying and destroying
operator deleteis that the former receives avoid *, and the latter receives a pointer to the type of the object being deleted and a dummystd::destroying_delete_tparameter. - Like non-destroying
operator delete, destroyingoperator deletecan also take an optionalstd::size_tand/orstd::align_val_tparameter, in the same way. These mean the same thing they always did, and they go after the dummystd::destroying_delete_tparameter. - The destructor is not called prior to the destroying
operator deleterunning, so it is expected to do so itself. This also means that the object is still valid and can be examined prior to doing so. - With non-destroying
operator delete, callingdeleteon a derived object through a pointer to a base class without a virtual destructor is Undefined Behavior. This can be made safe and well-defined by giving the base class a destroyingoperator delete, since its implementation can use other means to determine the correct destructor to call.
Use-cases for destroying operator delete were detailed in P0722R1. Here’s a quick summary:
- Destroying
operator deleteallows classes with variable-sized data at the end of them to retain the performance advantage of sizeddelete. This works by storing the size within the object, and retrieving it inoperator deletebefore calling the destructor. - If a class will have subclasses, any variable-sized data allocated at the same time must go before the start of the object, rather than after the end. In this case, the only safe way to
deletesuch an object is destroyingoperator delete, so that the correct starting address of the allocation can be determined. - If a class only has a few subclasses, it can implement its own dynamic dispatch for the destructor this way, instead of needing to use a vtable. This is slightly faster and results in a smaller class size.
Here’s an example of the third use case:
#include <iostream>
#include <new>
struct Shape {
const enum Kinds {
TRIANGLE,
SQUARE
} kind;
Shape(Kinds k) : kind(k) {}
~Shape() {
std::cout << "In Shape::~Shape()\n";
}
void operator delete(Shape *, std::destroying_delete_t);
};
struct Triangle : Shape {
Triangle() : Shape(TRIANGLE) {}
~Triangle() {
std::cout << "In Triangle::~Triangle()\n";
}
};
struct Square : Shape {
Square() : Shape(SQUARE) {}
~Square() {
std::cout << "In Square::~Square()\n";
}
};
void Shape::operator delete(Shape *p, std::destroying_delete_t) {
switch(p->kind) {
case TRIANGLE:
static_cast<Triangle *>(p)->~Triangle();
break;
case SQUARE:
static_cast<Square *>(p)->~Square();
}
::operator delete(p);
}
int main() {
Shape *p = new Triangle;
delete p;
p = new Square;
delete p;
}
It prints this:
In Triangle::~Triangle()
In Shape::~Shape()
In Square::~Square()
In Shape::~Shape()
(Note: GCC 11.1 and older will incorrectly call Triangle::~Triangle() instead of Square::~Square() when optimizations are enabled. See comment 2 of bug #91859.)