【问题标题】:What is "destroying operator delete" in C++20?C ++ 20中的“破坏运算符删除”是什么?
【发布时间】:2021-08-08 06:08:52
【问题描述】:

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

这究竟是什么,什么时候有用?

【问题讨论】:

标签: c++ destructor c++20 delete-operator destroy


【解决方案1】:

在 C++20 之前,对象的析构函数总是在调用 operator delete 之前调用。通过在 C++20 中销毁 operator deleteoperator 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的签名区别在于前者接收一个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 11.1 及更早版本将错误地调用Triangle::~Triangle() 而不是Square::~Square()。请参阅comment 2 of bug #91859。)

【讨论】:

  • “销毁 delete 使得通过指向基类的指针删除派生类是安全的,即使它没有虚拟析构函数。” - 是不是将确保安全的责任交给了破坏性删除的实施者?该函数现在必须以某种方式调用正确的析构函数。
  • 也可以用来实现侵入式指针,意思是只有在没有所有者离开的情况下才会进行实际删除?
  • @Deduplicator:在实践中可能是的,但形式上不是,除非对对象生存期的措辞和delete 运算符的有效操作数进行一些进一步的更改。
猜你喜欢
  • 1970-01-01
  • 2021-04-15
  • 2019-06-14
  • 2012-02-15
  • 2020-11-21
  • 2020-07-14
  • 2011-12-28
  • 2010-12-11
相关资源
最近更新 更多