我们真的需要放置新表达式吗?

Do we really need placement new-expressions?

我正在尝试理解 C++ 中的 placement new-expressions

表示 T* p = new T(arg); 等同于

void* place = operator new(sizeof(T));  // storage allocation
T* p = new(place) T(arg);               // object construction

并且 delete p; 等同于

p->~T();             // object destruction
operator delete(p);  // storage deallocation

为什么我们需要在T* p = new(place) T(arg);中放置new-expression来构造对象,下面不是等价的吗?

T* p = (T*) place;
*p = T(arg);

Placement new 有它的用例。一个例子是小缓冲区优化以避免堆分配:

struct BigObject
{
    std::size_t a, b, c;
};

int main()
{    
    std::byte buffer[24];

    BigObject* ptr = new(buffer) BigObject {1, 2, 3};

    // use ptr ...

    ptr->~BigObject();
}

此示例将在 buffer 中创建一个 BigObject 实例,它本身是一个位于堆栈上的对象。如您所见,我们自己没有在这里分配任何内存,因此我们也没有释放它(我们没有在这里调用 delete)。但是我们仍然需要通过调用析构函数来销毁对象。

在您的特定示例中放置 new 没有多大意义,因为您基本上是自己完成 new 运算符的工作。但是一旦将内存分配和对象构造分开,就需要放置新的。


至于你的

T* p = (T*) place;
*p = T(arg);

示例:正如 Evg 已经在评论中提到的,您正在取消引用指向未初始化内存的指针。 p 尚未指向 T 对象,因此取消引用它是 UB。

一个示例用例是包含非平凡类型的联合。您将必须显式构造非平凡成员并显式销毁它:

#include <iostream>

struct Var {
    enum class Type { INT = 0, STRING } type;
    union { int val; std::string name; };
    Var(): type(Type::INT), val(0) {}
    ~Var() { if (type == Type::STRING) name.~basic_string(); }
    Var& operator=(int i) {
        if (type == Type::STRING) {
            name.~basic_string();  // explicit destruction required
            type = Type::INT;
        }
        val = i;
        return *this;
    }
    Var& operator=(const std::string& str) {
        if (type != Type::STRING) {
            new (&name) std::string(str);  // in-place construction
            type = Type::STRING;
        } else
            name = str;
        return *this;
    }
};

int main() {
    Var var;      // var is default initialized with a 0 int
    var = 12;     // val assignment
    std::cout << var.val << "\n";
    var = "foo";  // name assignment
    std::cout << var.name << "\n";
    return 0;
}

从 C++17 开始,我们有 std::variant class 在后台执行此操作,但如果您使用 C++14 或更早版本,则必须执行此操作手工

顺便说一句,真实世界 class 应该包含一个流注入器和提取器,并且应该有 getter 能够在您不访问当前值时引发异常。为简洁起见,此处省略了它们……

首先要注意的是*p = T(arg);是赋值,不是构造。

现在让我们阅读标准([basic.life]/1):

... The lifetime of an object of type T begins when:

  • storage with the proper alignment and size for type T is obtained, and
  • its initialization (if any) is complete (including vacuous initialization)

对于通用类型T,如果使用放置new,初始化可能已经完成,但事实并非如此。正在做

void* place = operator new(sizeof(T));
T* p = (T*)place;

不会开始 *p 的生命周期。

同一节内容为 ([basic.life]/6):

... Before the lifetime of an object has started but after the storage which the object will occupy has been allocated ... any pointer that represents the address of the storage location where the object will be ... located may be used but only in limited ways. ... The program has undefined behavior if: ...

  • the pointer is used to access a non-static data member or call a non-static member function of the object, ...

operator= 是一个非静态成员函数,执行 *p = T(arg);,相当于 p->operator=(T(arg)),会导致未定义的行为。

一个简单的例子是 class,它包含一个指针作为数据成员,它在构造函数中初始化并在赋值运算符中取消引用。如果没有放置 new,构造函数将不会被调用,该指针也不会被初始化 (complete example)。