什么是 C++20 中的“销毁运算符删除”?

Jos*_*ica 57 c++ destructor destroy delete-operator c++20

C ++ 20 引入了“销毁operator delete”:operator delete采用标记类型std::destroying_delete_t参数的新重载。

这到底是什么,什么时候有用?

Jos*_*ica 62

在 C++20 之前,对象的析构函数总是在调用它们的operator delete. 使用operator deleteC++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;
}
Run Code Online (Sandbox Code Playgroud)

和输出:

#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;
}
Run Code Online (Sandbox Code Playgroud)

关于它的关键事实:

  • 销毁operator delete函数必须是类成员函数。
  • 如果有多个operator delete可用,破坏者总是优先于非破坏者。
  • non-destroying 和 destroying 签名的区别在于operator delete,前者接收一个void *,后者接收一个指向被删除对象类型的指针和一个哑std::destroying_delete_t参数。
  • 与 non-destroying 一样operator delete,destroyingoperator delete也可以以相同的方式采用可选std::size_t和/或std::align_val_t参数。这些意味着他们总是做同样的事情,他们追求虚拟std::destroying_delete_t参数。
  • 在销毁operator delete运行之前不会调用析构函数,因此预计它会自己调用。这也意味着对象仍然有效并且可以在这样做之前进行检查。
  • 对于 non-destroying operator deletedelete通过指向没有虚拟析构函数的基类的指针调用派生对象是未定义行为。这可以通过给基类一个 destroying 来变得安全和明确定义operator delete,因为它的实现可以使用其他方法来确定要调用的正确析构函数。

销毁用例operator deleteP0722R1 中有详细说明。这是一个快速总结:

  • 销毁operator delete允许在其末尾具有可变大小数据的类保留 sized 的性能优势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;
}
Run Code Online (Sandbox Code Playgroud)

它打印这个:

In Foo::~Foo()
In Foo::operator delete(void *)
In Bar::operator delete(Bar *, std::destroying_delete_t)
In Bar::~Bar()
Run Code Online (Sandbox Code Playgroud)

(注意:GCC 11.1 及更早版本将错误地调用Triangle::~Triangle()而不是Square::~Square()在启用优化时调用。请参阅错误 #91859 的评论 2。

  • “销毁删除可以安全地通过指向基类的指针删除派生类,即使它没有虚拟析构函数。” - 难道它不是把保证其安全的责任交给了破坏性删除的实施者吗?该函数现在必须*以某种方式*调用正确的析构函数。 (6认同)