在初始化对象时,丢弃placement new的返回值是否可以?

11

这个问题起源于这个帖子的评论区,并且在那里也有答案。然而,我认为这个问题太重要了,不能仅仅留在评论区。所以我创建了这个问答。

定位 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
根据 cppref,如果提供了放置参数,它们将作为附加参数传递给分配函数。这样的分配函数被称为“放置 new”,是指标准分配函数 void* operator new(std::size_t, void*),其仅以不变形式返回第二个参数。这用于在分配的存储中构造对象 [...]。这意味着 new(p) vec_t{1, 2, 3} 简单地返回 pp = new(p) vec_t{1, 2, 3} 看起来多余。忽略返回值真的没问题吗?

是的,就我所知这是常见的做法。 - DeiDei
只有放置数组新的问题。但是那个本身在可移植性方面无法使用。 - Deduplicator
1
@DeiDei 我之前也是这样认为的... 直到今天 :) - Lingxi
1
https://dev59.com/y1YN5IYBdhLWcg3wTmm-#48164192 - T.C.
1个回答

9
忽略返回值在严谨和实际上都是不可以的。
从严谨的角度来看,对于

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是否为聚合类型。

1
@underscore_d 你说得有道理。我们需要一位语言律师在这里。我会更新答案以提及这个问题。希望一些专家能够注意并解释。 - Lingxi
1
据我所知,放置 operator new 的结果不变,但 new 表达式的结果是指向新构造对象的指针。请参见[expr.new]/1。这应该至少足以满足需要使用 launder 的情况。 - Daniel H
3
非分配全局分配函数返回其参数,但new-expression始终返回指向创建的对象的指针,即使它恰好使用该分配函数。在C++抽象机器中,指针具有certain possible values。它的值不会在certain limited cases not applicable here之外神奇地改变。原始的p指向一些已分配的存储空间,而不是一个T对象,并且不能用于访问一个。 - T.C.
1
实际上,如果分配函数是不透明的,并且 T 没有 const/reference 成员,那么实现可能无法证明 p 实际上没有指向从 operator new 调用返回的 T 对象,而且如果它确实指向这样的对象,你可以使用原始指针来操作,因此在这种情况下你可能会得逞。 - T.C.
1
我想指出有时存储指针是不切实际的,所以我认为 launder 可能是更好的解决方案。例如,如果缓冲区是一个类成员,你可以通过取该成员的地址来获取指针。将放置 new 的返回值存储在另一个类成员中将浪费空间。 - nog642
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接