初始化对象时是否可以放弃放置新的返回值

Lin*_*gxi 7 c++ initialization placement-new compiler-optimization

这个问题源于这个帖子中的评论部分,并且在那里也得到了答案.但是,我认为仅仅留在评论部分是非常重要的.所以我为此做了这个Q&A.

Placement new可用于初始化分配的存储中的对象,例如,

using vec_t = std::vector<int>;
auto p = (vec_t*)operator new(sizeof(vec_t));
new(p) vec_t{1, 2, 3}; // initialize a vec_t at p
Run Code Online (Sandbox Code Playgroud)

根据cppref,

安置新的

如果提供了placement_params,则将它们作为附加参数传递给分配函数.在标准分配函数之后,这种分配函数被称为"placement new" void* operator new(std::size_t, void*),它只是简单地返回其第二个参数.这用于在分配的存储中构造对象[...]

这意味着new(p) vec_t{1, 2, 3}简单地返回p,并且p = new(p) vec_t{1, 2, 3}看起来多余.忽略返回值真的可以吗?

Lin*_*gxi 9

无论是迂腐还是实际,忽略返回值都不行.

从迂腐的角度来看

For p = new(p) T{...},p作为指向new-expression创建的对象的指针new(p) T{...},尽管该值相同,但仍不适用.在后一种情况下,它仅限定为指向已分配存储的指针.

非分配全局分配函数返回其参数而没有隐含的副作用,但是new-expression(placement或not)总是返回指向它创建的对象的指针,即使它恰好使用了该分配函数.

每个cppref关于delete-expression的描述(强调我的):

对于第一个(非数组)形式,expression必须是指向对象类型的指针或上下文可隐式转换为此类指针的类类型,并且其值必须为null或指向由new创建的非数组对象的指针表达式,或指向由new-expression创建的非数组对象的基础子对象的指针.如果expression是其他任何东西,包括它是否是由new-expression的数组形式获得的指针,则行为是未定义的.

p = new(p) T{...}因此未能做出delete p未定义的行为.

从实际的角度来看

从技术上讲,尽管值(内存地址)是相同的,但没有p = new(p) T{...},p也没有指向新初始化T的.因此,编译器可以假设p仍然引用T放置new之前的那个.考虑一下代码

p = new(p) T{...} // (1)
...
new(p) T{...} // (2)
Run Code Online (Sandbox Code Playgroud)

甚至在之后(2),编译器可能会假设p仍然引用初始化的旧值(1),从而进行不正确的优化.例如,如果T有一个const成员,编译器可能会缓存其值,(1)并且即使在之后仍然使用它(2).

p = new(p) T{...}有效地禁止这种假设.另一种方法是使用std::launder(),但只是将放置新的返回值分配给更简单,更简洁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)...};
}
Run Code Online (Sandbox Code Playgroud)

这些函数模板始终在内部设置指针.与std::is_aggregate自C++ 17可用时,溶液可通过之间自动选择来改善(){}基于是否语法T是一个聚合类型.

  • 非分配全局分配函数返回其参数,但*new-expression*总是返回指向它创建的对象的指针,即使它碰巧使用该分配函数.C++抽象机器中的指针具有[某些可能的值](https://timsong-cpp.github.io/cppwp/basic.compound#3).它的价值并没有神奇地改变[在这里不适用的某些有限情况之外](https://timsong-cpp.github.io/cppwp/basic.life#8).原始的`p`指向一些已分配的存储,而不是`T`对象,并且不能用于访问一个. (2认同)