可以将堆栈对象地址提供给 placement new 吗?

Is it okay to give a stack object address to placement new?

忽略这种做法的用处。 (当然,我们欢迎现实生活中的例子。)

例如,以下程序输出 a 的正确值:

#include <iostream>

using namespace std;

int main()
{
  int a = 11111;
  int i = 30;

  int* pi = new (&i) int();

  cout << a << " " << endl;
}

但是新分配不应该在 i 附近创建一些簿记信息(用于正确的后续释放),在这种情况下应该破坏 i 周围的堆栈?

是的,使用指向堆栈上对象的指针执行 placement-new 是完全可以的。它只会使用那个特定的指针来构造对象。 Placement-new 实际上并不是 分配 任何内存 - 你已经提供了那部分。它只做构造。随后的删除实际上不会是 delete - there is no placement delete - 因为您需要做的就是调用对象的析构函数。实际内存由其他东西管理 - 在本例中是您的堆栈对象。

例如,给定这个简单类型:

struct A {
    A(int i) 
    : i(i)
    {
        std::cout << "make an A\n";
    }

    ~A() {
        std::cout << "delete an A\n";
    }

    int i;
};

以下是完全合理、行为良好的代码:

char buf[] = {'x', 'x', 'x', 'x', 0};
std::cout << buf << std::endl;  // xxxx
auto a = new (buf) A{'a'};      // make an A
std::cout << a->i << std::endl; // 97
a->~A();                        // delete an A

唯一无效的情况是,如果你的 placement-new-ed 对象比你 new-ed 它的内存更持久 - 出于同样的原因,返回一个悬空指针总是不好的:

A* getAnA(int i) {
    char buf[4];
    return new (buf) A(5); // oops
}

Placement new 做的是构造,不是分配,所以不用担心记账信息。

我现在可以想到一个可能的用例,尽管它(以这种形式)将是一个糟糕的封装示例:

#include <iostream>
using namespace std;
struct Thing {
 Thing (int value) {
  cout << "such an awesome " << value << endl;
 }
};
union Union {
 Union (){}
 Thing thing;
};
int main (int, char **) {
 Union u;
 bool yes;
 cin >> yes;
 if (yes) {
  new (&(u.thing)) Thing(42);
 }
 return 0;
}

Live here

尽管即使在某些成员函数中隐藏了 placement new,构造仍然发生在堆栈上。

所以:我没有看标准,但想不出为什么不允许在堆栈上放置 new。

一个真实世界的例子应该在 https://github.com/beark/ftl 的源代码中的某个地方......在它们的递归联合中,它用于求和类型。

Placement new 就地构造元素,不分配内存。

本例中的"bookkeeping information"是返回的指针,应该用于销毁放置的对象。

由于放置是一种构造,因此没有与放置关联的删除。因此,放置 new 所需的 "clean up" 操作是销毁。

"usual steps" 是

  1. 'Allocate'内存
  2. 就地构建元素
  3. 做事
  4. 销毁元素(2 的反向)
  5. 'Deallocate'内存(1的倒数)

(其中内存可以是既不需要显式分配也不需要释放但与堆栈数组或对象一起出现和消失的堆栈内存。)

注意:如果 "placing" 一个对象进入堆栈中相同类型的内存,应该记住在对象生命周期结束时会自动销毁。

{ 
  X a;
  a.~X(); // destroy a
  X * b = new (&a) X(); // Place new X
  b->~X(); // destroy b
} // double destruction

不,因为你没有delete一个已经被放置新的对象,你手动调用它的析构函数。

struct A {
    A() { std::cout << "A()\n"; }
    ~A() { std::cout << "~A()\n"; }
};

int main()
{
    alignas(A) char storage[sizeof(A)];

    A *a = new (storage) A;
    std::cout << "hi!\n";

    a->~A();
    std::cout << "bye!\n";
}

输出:

A()
hi!
~A()
bye!

在你的情况下,也没有必要调用析构函数,因为 int 是微不足道的可破坏的,这意味着它的析构函数无论如何都是空操作。

注意不要在仍然存在的对象上调用 placement-new,因为你不仅会破坏它的状态,而且它的析构函数也会被调用两次(一次当你手动调用时,还有当原始对象应该已被删除,例如在其范围的末尾)。