Что такое «уничтожающий оператор delete» в C++ 20?

В C++ 20 введено «уничтожение operator delete»: новые перегрузки, operator delete которые принимают std::destroying_delete_tпараметр типа тега .

Что это такое и когда это полезно?

Ответов (1)

Решение

До C++ 20 деструкторы объектов всегда вызывались перед их вызовом operator delete . С уничтожением operator delete в C++ 20 operator delete вместо этого может вызывать сам деструктор. Вот очень простой игрушечный пример неразрушающего и разрушающего 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;
}

И вывод:

In Foo::~Foo()
In Foo::operator delete(void *)
In Bar::operator delete(Bar *, std::destroying_delete_t)
In Bar::~Bar()

Основные факты об этом:

  • Разрушающая operator deleteфункция должна быть функцией-членом класса.
  • Если operator deleteдоступно более одного , уничтожающий всегда будет иметь приоритет над неразрушающим.
  • Разница между сигнатурами неразрушения и уничтожения operator deleteзаключается в том, что первая получает a void *, а вторая получает указатель на тип удаляемого объекта и фиктивный std::destroying_delete_tпараметр.
  • Подобно неразрушению operator delete, уничтожение operator deleteтакже может принимать необязательный std::size_tи / или std::align_val_tпараметр таким же образом. Они означают то же самое, что и всегда, и идут после фиктивного std::destroying_delete_tпараметра.
  • Деструктор не вызывается перед operator deleteзапуском уничтожения , поэтому ожидается, что он сделает это сам. Это также означает, что объект по-прежнему действителен и может быть исследован перед этим.
  • При неразрушении operator deleteвызов deleteпроизводного объекта через указатель на базовый класс без виртуального деструктора является неопределенным поведением. Это можно сделать безопасным и четко определенным, задав базовому классу уничтожение operator delete, поскольку его реализация может использовать другие средства для определения правильного деструктора для вызова.

Сценарии использования для уничтожения operator delete подробно описаны в P0722R1 . Вот краткое изложение:

  • Уничтожение operator deleteпозволяет классам с данными переменного размера в конце сохранять преимущество в производительности, связанное с размером delete. Это работает, сохраняя размер внутри объекта и извлекая его operator deleteперед вызовом деструктора.
  • Если у класса есть подклассы, любые данные переменного размера, выделенные одновременно, должны располагаться до начала объекта, а не после его конца. В этом случае единственный безопасный путь к deleteтакому объекту - уничтожение operator delete, чтобы можно было определить правильный начальный адрес выделения.
  • Если у класса есть только несколько подклассов, он может реализовать свою собственную динамическую диспетчеризацию для деструктора таким образом, вместо того, чтобы использовать vtable. Это немного быстрее и приводит к меньшему размеру класса.

Вот пример третьего варианта использования:

#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;
}

Он печатает это:

In Triangle::~Triangle()
In Shape::~Shape()
In Square::~Square()
In Shape::~Shape()

(Примечание: в настоящее время GCC будет вызывать некорректно, Triangle::~Triangle() а не Square::~Square() тогда, когда включена оптимизация. См. Комментарий 2 к ошибке № 91859. )