placement-new 成同类型还需要手动调用析构函数吗?

Does placement-new into the same type still require to manually call the destructor?

上下文

我正试图掌握新的放置机制,因为我从来没有用过它。出于纯粹的好奇心,我试图了解如何正确使用它。

对于这个问题,我们将考虑以下代码库以供说明:

struct Pack
{
    int a, b, c, d;
    Pack() = default;
    Pack(int w, int x, int y, int z) : a(w), b(x), c(y), d(z)
    {}
    ~Pack()
    {
        std::cout << "Destroyed\n";
    }
};

std::ostream & operator<<(std::ostream & os, const Pack & p)
{
    os << '[' << p.a << ", " << p.b << ", " << p.c << ", " << p.d << ']';
    return os;
}

基本上它只是定义了一个包含一些数据的结构(示例中为 4 个整数),我重载了 operator<<() 以简化测试部分的代码。

我知道在使用 placement-new 时,必须手动调用对象的析构函数,因为我们不能使用 delete 因为我们只想销毁对象(而不是释放内存因为它已经被分配了)。

示例(A):

char b[sizeof(Pack)];
Pack * p = new (b) Pack(1, 2, 3, 4);
std::cout << *reinterpret_cast<Pack*>(b) << '\n';

p->~Pack();

问题

我在想,在同类型的对象中使用placement-new时,是否还需要调用对象的析构函数?

示例(B):

Pack p;
new (&p) Pack(1, 2, 3, 4);
std::cout << p << '\n';

我做了一个快速的 test of both version here 并且当基础对象超出范围并且类型为 Pack 时,似乎正确调用了析构函数。
我知道在这样一个微不足道的例子中使用 placement-new 而不是直接创建对象是愚蠢的。但在实际例子中,它可以是缓冲区或 Pack 而不是 char.

的缓冲区

我想知道我假设我不需要手动调用示例中的析构函数是否正确 (B) 或者我是否遗漏了什么.


额外

作为一个附属问题,在例子(A)中,通过reinterpret_casted指针去掉返回指针调用析构函数是否合法相反?

比如可以这样改写吗:

char b[sizeof(Pack)];
new (b) Pack(1, 2, 3, 4);
std::cout << *reinterpret_cast<Pack*>(b) << '\n';

reinterpret_cast<Pack*>(b)->~Pack(); // Is it legal ?

如果我有一个对象缓冲区并且我想销毁它的元素而不必将返回的指针保存在某个地方,这将很有用。

[免责声明]:当然,在实际案例程序中,我会使用 std::vectoremplace_back() 函数而不是我自己的缓冲区。如前所述,这个问题纯粹是出于好奇,只是为了了解它是如何在幕后工作的。

是的,你必须调用析构函数。旧对象是否同类型无关

唯一重要的是旧对象的类型是否可以轻易破坏。如果是,则无需调用析构函数。如果不是,则必须在重新使用内存之前调用析构函数。

示例:

Pack p;

new (&p) Pack(1, 2, 3, 4); // Not OK

p.~Pack();
new (&p) Pack(1, 2, 3, 4); // OK

请注意,在某些情况下这是不允许的,例如 class 包含 const 限定成员或引用成员。一般来说,我建议避免这种模式,而是 re-use 仅使用 char 数组或类似的琐碎存储。

As a subsidiary question, in the example (A), is it legal to get rid of the returned pointer and call the destructor through the reinterpret_casted pointer instead ?

就像 placement-newed 对象的所有用途一样,您可以重新解释原始对象的地址,但必须清洗它:

Pack* ptr = new (b) Pack(1, 2, 3, 4);

std::cout << *ptr << '\n'; // OK
std::cout << *reinterpret_cast<Pack*>(b) << '\n'; // Not OK
std::cout << *std::launder(reinterpret_cast<Pack*>(b)) << '\n'; // OK

ptr->~Pack(); // OK
reinterpret_cast<Pack*>(b)->~Pack(); // Not OK
std::launder(reinterpret_cast<Pack*>(b))->~Pack(); // OK

char b[sizeof(Pack)];
Pack * p = new (b) Pack(1, 2, 3, 4);

这是错误的。您必须确保存储正确对齐 placement-newed 类型:

alignas(alignof(Pack)) char b[sizeof(Pack)];

不调用析构函数是不是未定义的行为。但是如果你依赖析构函数的副作用,它可能会导致未定义的行为。

在您的示例 (a) 中,创建了两个 Pack 对象。一个是在 Pack p; 处默认构建的。当您使用 new (b) Pack(1, 2, 3, 4); 重用其存储时,此对​​象的生命周期结束,在同一地址开始另一个 Pack 对象的生命周期,并以显式调用析构函数结束。请注意,调用了 2 个构造函数,但只调用了 1 个析构函数。如果这个 class 持有一些资源(比如 std::vectorstd::string),它可能会导致内存泄漏,但这本身并不是 UB。

如果析构函数很简单,就没有副作用,所以跳过析构函数调用总是可以的。