What is "destroying operator delete" in C++20?
Asked Answered
L

1

61

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?

Locker answered 19/5, 2021 at 1:26 Comment(2)
This post is being discussed on meta in Why is this c++ question which asks “what destroying operator deletes does” an acceptable question?Towage
Related #66512329Sproul
L
71

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.)

Locker answered 19/5, 2021 at 1:26 Comment(4)
"Destroying delete makes it safe to delete a derived class through a pointer to a base class even if it doesn't have a virtual destructor." - Isn't it rather that it puts the onus of making it safe on the implementer of the destroying delete? The function must now call the correct destructor somehow.Athenian
Can that also be used for implementing intrusive pointers, meaning actual deletion will only be done if no owners left?Wrinkly
@Deduplicator: Probably yes in practice but formally no, unless some further changes are made to the verbiage around object lifetime and valid operands to the delete operator.Amosamount
GCC still shows a warning if one uses destroying operator delete: gcc.gnu.org/bugzilla/show_bug.cgi?id=100861Cherimoya

© 2022 - 2024 — McMap. All rights reserved.