我正在努力理解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。