C++20 new feature of destroying operator delete, allows to "hook" the call to the destructor and "replace" it with our own operation (e.g. theoretically, calling the proper destructor of a derived class).
Is it possible to use destroying operator delete, to allow a unique_ptr<A>
to hold a pointer to an actual non-polymorphic derived class of A
(i.e. no virtual destructor in A
) without a need for a custom deleter?
Yes, it is possible. In fact it resembles a use case raised in P0722 for Dynamic dispatch without vptrs.
Before C++20 A unique_ptr<A>
holding a pointer to a derived class of A
required either:
A
-- ORThe C++20 specification added new delete operator - destroying operator delete: calling delete on a static type which is different from the dynamic type to be deleted, does not fall in the undefined behavior case, if the selected deallocation function is a destroying operator delete, as detailed in [expr.delete](§7.6.2.9/3) (emphasis mine):
In a single-object delete expression, if the static type of the object to be deleted is different from its dynamic type and the selected deallocation function [...] is not a destroying operator delete, the static type shall be a base class of the dynamic type of the object to be deleted and the static type shall have a virtual destructor or the behavior is undefined. [...]
So, the option of using destroying operator delete for this purpose is valid.
For example, if we know that A
would never be instantiated and that unique_ptr<A>
would actually always hold a pointer of type A_Proxy
, we can perform a down cast in the destroying operator delete by using static_cast
and call the proper destructor (the same could have been done in a custom deleter, which can be waived now).
class A {
friend struct A_Proxy;
std::string s; // just an example of a member managed at A's level
A(const char* str): s(str) {}
~A() {}
public:
// Note: this is the destroying operator delete, as introduced in C++20
void operator delete(A *p, std::destroying_delete_t);
static std::unique_ptr<A> create(); // no need for a custom deleter
void foo() const;
};
A simple derived class A_Proxy:
struct A_Proxy: A {
A_Proxy(): A("A_Proxy") {}
~A_Proxy() { /* do anything that is required at the proxy level */ }
void foo() const {
std::cout << "A_Proxy::foo()" << std::endl;
}
};
With the implementations of A
:
void A::operator delete(A *p, std::destroying_delete_t) {
// in this example we know for sure p is of type A_Proxy*
::delete static_cast<A_Proxy*>(p);
// ^ call the global ::delete to avoid recursion
}
std::unique_ptr<A> A::create() {
return std::make_unique<A_Proxy>(); // without the need for a custom deleter
}
void A::foo() const {
static_cast<const A_Proxy*>(this)->foo();
}
Main:
int main () {
auto a = A::create();
auto b = a.release();
a = std::unique_ptr<A>(b);
a->foo();
}
The code above - with a destroying operator delete.
It is to be noted however that there is no real magic here. Using a custom deleter would result with quite a similar code. Note also that most if not all implementations of unique_ptr
would have a bare pointer size for a stateless custom deleter, which can be used for this purpose.
The same code as above - but with a custom deleter.
<= Note that the size of the unique_ptr
with the custom deleter in this implementation is the same as a bare pointer.
Above technique can be relevant also in cases where there is more than a single possible cast, by using a proper type flag in the base class, for example:
void A::operator delete(A *p, std::destroying_delete_t) {
if(p->type == "A") {
::delete p;
}
else if(p->type == "B") {
::delete static_cast<B*>(p);
}
else if(p->type == "C") {
::delete static_cast<C*>(p);
}
else {
throw "unsupported type";
}
}
And again, both approaches - the destroying operator delete approach and the custom deleter approach would result with quite a similar code and a bare pointer size for the unique_ptr
(in the destroying operator delete approach the unique_ptr
size is ensured to be of bare pointer size, in the custom deleter approach it would most probably be, if you implement the deleter properly, and depending on the actual implementation of unique_ptr
).