忽略返回值在严谨和实际上都是不可以的。
从严谨的角度来看,对于
p = new(p) T{...}
,
p
被认定为由 new-expression 创建的对象的指针,而对于
new(p) T{...}
,尽管值相同,但它仅被认定为已分配存储的指针。
非分配全局分配函数会无副作用地返回其参数,但是新表达式(无论是否放置)始终返回它创建的对象的指针,即使它恰好使用该分配函数。
根据cppref关于
delete-expression的描述(重点是我的):
对于第一个(非数组)形式,
expression 必须是指向对象类型或类类型的指针,可以在上下文中隐含地转换为这种指针,
它的值必须是null或由new-expression创建的非数组对象的指针,或者是new-expression创建的非数组对象的基类子对象的指针。 如果
expression 是其他任何内容,包括通过new-expression的数组形式获得的指针,则其行为是未定义的。
因此,如果未能执行 p = new(p) T{...}
,则 delete p
的行为是未定义的。
从实际角度来看
从技术上讲,如果没有 p = new(p) T{...}
,尽管值(内存地址)相同,但 p
实际上并不指向新初始化的 T
。编译器可能会假设 p
仍然指向放置 new 前的 T
。考虑以下代码:
p = new(p) T{...} // (1)
...
new(p) T{...} // (2)
即使经过(2)步骤,编译器仍可能假定p仍然引用在(1)步骤初始化的旧值,并因此进行不正确的优化。例如,如果T有一个const成员,则编译器可能会在(1)处缓存其值并在(2)步骤后仍使用它。
p = new(p) T{...}
有效地禁止了这种假设。另一种方法是使用std::launder()
,但更容易和更清晰的方法是将placement new的返回值分配回p
。
避免陷阱的方法
template <typename T, typename... Us>
void init(T*& p, Us&&... us) {
p = new(p) T(std::forward<Us>(us)...);
}
template <typename T, typename... Us>
void list_init(T*& p, Us&&... us) {
p = new(p) T{std::forward<Us>(us)...};
}
这些函数模板总是在内部设置指针。自C++17以来,有
std::is_aggregate
可用,可以通过自动选择
()
和
{}
语法来改进解决方案,具体取决于
T
是否为聚合类型。