What is "destroying operator delete" in C++20?

C++DestructorC++20Delete OperatorDestroy

C++ Problem Overview


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++ Solutions


Solution 1 - C++

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 a void *, and the latter receives a pointer to the type of the object being deleted and a dummy std::destroying_delete_t parameter.
  • Like non-destroying 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.
  • 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, 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:

  • Destroying 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.
  • 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 destroying operator 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.)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJoseph Sible-Reinstate MonicaView Question on Stackoverflow
Solution 1 - C++Joseph Sible-Reinstate MonicaView Answer on Stackoverflow