What is "destroying operator delete" in C++20?
C++20 introduced "destroying operator delete
": new overloads of operator delete
that take a tag-type std::destroying_delete_t
parameter.
What exactly is this and when is it useful?
Solution 1:
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 delete
function must be a class member function. - If more than one
operator delete
is available, a destroying one will always take precedence over a non-destroying one. - The difference between the signatures of non-destroying and destroying
operator delete
is that the former receives avoid *
, and the latter receives a pointer to the type of the object being deleted and a dummystd::destroying_delete_t
parameter. - Like non-destroying
operator delete
, destroyingoperator delete
can also take an optionalstd::size_t
and/orstd::align_val_t
parameter, in the same way. These mean the same thing they always did, and they go after the dummystd::destroying_delete_t
parameter. - The destructor is not called prior to the destroying
operator delete
running, 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
, callingdelete
on 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 delete
allows 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 delete
before 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
delete
such 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.)