Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 541
Joseph Sible-Reinstate Monica Avatar asked May 19 '21 01:05

Joseph Sible-Reinstate Monica


People also ask

What is delete operator in C?

delete keyword in C++ Delete is an operator that is used to destroy array and non-array(pointer) objects which are created by new expression. Delete can be used by either using Delete operator or Delete [ ] operator. New operator is used for dynamic memory allocation which puts variables on heap memory.

What happens when the operator is deleted?

Using the delete operator on an object deallocates its memory. A program that dereferences a pointer after the object is deleted can have unpredictable results or crash.


1 Answers

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

like image 132
Joseph Sible-Reinstate Monica Avatar answered Oct 16 '22 19:10

Joseph Sible-Reinstate Monica