【问题标题】:Is it OK to discard placement new return value when initializing objects初始化对象时丢弃放置新返回值是否可以
【发布时间】:2018-09-09 03:59:44
【问题描述】:

这个问题来自this线程的评论部分,并且在那里也得到了答案。但是,我认为只留在评论部分太重要了。所以我为它做了这个问答。

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

根据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} 看起来是多余的。忽略返回值真的可以吗?

【问题讨论】:

  • 是的,据我所知,这是常见的做法。
  • 只有placement array new 有问题。但无论如何,那个本身就无法携带。
  • @DeiDei 我也这么认为...直到今天:)

标签: c++ initialization compiler-optimization placement-new


【解决方案1】:

不管是学究式还是实际操作,忽略返回值都是不行的。

从迂腐的角度来看

对于p = new(p) T{...}p 有资格作为指向由 new 表达式创建的对象的指针,尽管值相同,但它不适用于 new(p) T{...}。在后一种情况下,它只能作为指向已分配存储的指针。

非分配全局分配函数返回它的参数,没有暗示任何副作用,但是一个新表达式(无论是否放置)总是返回一个指向它创建的对象的指针,即使它碰巧使用了那个分配函数。

根据 cppref 关于delete-expression 的描述(强调我的):

对于第一种(非数组)形式,表达式必须是指向对象类型的指针,或者是上下文隐式可转换为此类指针的类类型,其值必须是null 或指向由 new 表达式创建的非数组对象的指针,或指向由 new 表达式创建的非数组对象的基本子对象的指针。 如果 表达式 是其他任何东西,包括如果它是通过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 是否为聚合类型自动选择 (){} 语法来改进解决方案。

【讨论】:

  • 我绝不是指针炼金术方面的专家,但我也不相信这里有限的推理和缺乏对其他讨论的参考。我没有理由认为您引用的内容,'指向由new 创建的事物的指针' 必然与您得出的结论相同,'返回的指针new';我认为 "created by" 在语法上绑定到对象,而不是指针。如果您不获取返回值,p 仍将指向该对象,因为根据定义,new 必须在 p 处创建对象。
  • @underscore_d 你说得有道理。我们这里需要一位语言律师。我将更新答案以提及此问题。希望有专家能注意到并解释。
  • @underscore_d 据我了解,operator new 放置的结果不变,但new 表达式的结果是指向新构造对象的指针。请参阅 [[expr.new]/1](timsong-cpp.github.io/cppwp/expr.new#1.sentence-7)。这至少应该足以满足launder-needed 的情况。
  • 非分配全局分配函数返回它的参数,但是一个 new-expression 总是返回一个指向它创建的对象的指针,即使它碰巧使用了那个分配函数. C++ 抽象机中的指针有certain possible values。它的值不会神奇地改变outside of certain limited cases not applicable here。原始的p 指向一些分配的存储,而不是T 对象,并且不能用于访问。
  • 实际上,如果分配函数是不透明的并且T没有const/reference成员,实现可能无法证明p实际上没有指向Toperator new 调用返回时的对象,并且由于您可以使用原始指针(如果它确实指向这样的对象),因此在这种情况下您可能会侥幸逃脱。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-05-02
  • 2022-08-14
  • 2019-05-09
  • 1970-01-01
  • 2012-03-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多