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?
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?
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:
operator delete
function must be a class member function.operator delete
is available, a destroying one will always take precedence over a non-destroying one.operator delete
is that the former receives a void *
, and the latter receives a pointer to the type of the object being deleted and a dummy std::destroying_delete_t
parameter.operator delete
, destroying operator delete
can also take an optional std::size_t
and/or std::align_val_t
parameter, in the same way. These mean the same thing they always did, and they go after the dummy std::destroying_delete_t
parameter.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.operator delete
, calling delete
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 destroying operator 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:
operator delete
allows classes with variable-sized data at the end of them to retain the performance advantage of sized delete
. This works by storing the size within the object, and retrieving it in operator delete
before calling the destructor.delete
such an object is destroying operator delete
, so that the correct starting address of the allocation can be determined.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.)
delete
operator. –
Amosamount © 2022 - 2024 — McMap. All rights reserved.