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



我正在努力理解C++中新表达式的位置。

此堆栈溢出回答表示T* p = new T(arg);等效于

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

并且CCD_ 2等效于

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

为什么我们需要在T* p = new(place) T(arg);中放置新的表达式来构建对象,这不是等价的吗?

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

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

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

。。。T类型对象的生存期从以下时间开始:

对于类型T,获得了具有适当排列和大小的
  • 存储,并且
  • 其初始化(如果有的话)已完成(包括真空初始化)

对于一般类型的T,如果使用放置new,则可以完成初始化,但事实并非如此。进行

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

不会启动CCD_ 9的生命周期。

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

。。。在对象的生存期开始之前,但在分配了对象将占用的存储之后。。。表示对象所在存储位置地址的任何指针。。。located可以使用,但只能以有限的方式使用。。。程序具有未定义的行为,如果:…

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

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

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

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

#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或更早的版本,则必须手动完成

BTW,一个真实世界的类应该包含一个流注入器和提取器,并且如果您不访问当前值,则应该有能够引发异常的getter。为了简洁起见,这里省略了它们…

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还没有指向delete p;0对象,因此取消引用它是UB。

最新更新