2

我试图了解C++ 中的放置新表达式

这个堆栈溢出答案状态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);对象构造中放置新表达式,以下不是等价的吗?

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

3 回答 3

5

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

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

...类型对象的生命周期T开始于:

  • 获得具有适当对齐和大小的类型T的存储,并且
  • 它的初始化(如果有的话)已经完成(包括空初始化)

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

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

不会开始*p.

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

...在对象的生命周期开始之前,但在分配对象将占用的存储空间之后...任何表示对象将...所在存储位置的地址的指针都可以使用,但仅限以有限的方式。...如果出现以下情况,则程序具有未定义的行为: ...

  • 指针用于访问非静态数据成员或调用对象的非静态成员函数,...

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

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

于 2021-09-13T08:24:14.360 回答
1

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

#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类,但如果您使用 C++14 或更早版本,则必须手动完成……</p>

顺便说一句,真实世界的类应该包含一个流注入器和提取器,并且如果您不访问当前值,应该让 getter 能够引发异常。为简洁起见,此处省略...</p>

于 2021-09-13T08:20:32.530 回答
0

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();
}

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

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


至于你的

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

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

于 2021-09-13T07:40:53.517 回答