初始化对象时是否可以丢弃 placement new return 值
Is it OK to discard placement new return value when initializing objects
这个问题来自跟帖的评论区,也在那里得到了答案。但是,我认为它太重要了,不能只留在评论部分。所以特意做了这个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
根据cppref,
Placement new
If placement_params are provided, they are passed to the allocation function as additional arguments. Such allocation functions are known as "placement new", after the standard allocation function void* operator new(std::size_t, void*)
, which simply returns its second argument unchanged. This is used to construct objects in allocated storage [...]
这意味着 new(p) vec_t{1, 2, 3}
只是 returns p
,而 p = new(p) vec_t{1, 2, 3}
看起来是多余的。忽略 return 值真的可以吗?
忽略 return 值在迂腐和实践上都不行。
迂腐的观点
对于 p = new(p) T{...}
,p
有资格作为指向由 new-expression 创建的对象的指针,这不适用于 new(p) T{...}
,尽管值是一样的。在后一种情况下,它只能作为指向已分配存储的指针。
non-allocating 全局分配函数 return 它的参数没有隐含的副作用,但是 new-expression (放置与否)总是 return 指向它创建的对象,即使它碰巧使用了那个分配函数。
根据 cppref 对 delete-expression 的描述(强调我的):
For the first (non-array) form, expression must be a pointer to a object type or a class type contextually implicitly convertible to such pointer, and its value must be either null or pointer to a non-array object created by a new-expression, or a pointer to a base subobject of a non-array object created by a new-expression. If expression is anything else, including if it is a pointer obtained by the array form of new-expression, the behavior is undefined.
未能 p = new(p) T{...}
因此会导致 delete p
未定义的行为。
从实用的角度来说
从技术上讲,如果没有 p = new(p) T{...}
,p
不会指向 newly-initialized 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 的 return 值分配回 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
是否为聚合类型自动在 ()
和 {}
语法之间选择来改进解决方案。
这个问题来自
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 new
If placement_params are provided, they are passed to the allocation function as additional arguments. Such allocation functions are known as "placement new", after the standard allocation function
void* operator new(std::size_t, void*)
, which simply returns its second argument unchanged. This is used to construct objects in allocated storage [...]
这意味着 new(p) vec_t{1, 2, 3}
只是 returns p
,而 p = new(p) vec_t{1, 2, 3}
看起来是多余的。忽略 return 值真的可以吗?
忽略 return 值在迂腐和实践上都不行。
迂腐的观点
对于 p = new(p) T{...}
,p
有资格作为指向由 new-expression 创建的对象的指针,这不适用于 new(p) T{...}
,尽管值是一样的。在后一种情况下,它只能作为指向已分配存储的指针。
non-allocating 全局分配函数 return 它的参数没有隐含的副作用,但是 new-expression (放置与否)总是 return 指向它创建的对象,即使它碰巧使用了那个分配函数。
根据 cppref 对 delete-expression 的描述(强调我的):
For the first (non-array) form, expression must be a pointer to a object type or a class type contextually implicitly convertible to such pointer, and its value must be either null or pointer to a non-array object created by a new-expression, or a pointer to a base subobject of a non-array object created by a new-expression. If expression is anything else, including if it is a pointer obtained by the array form of new-expression, the behavior is undefined.
未能 p = new(p) T{...}
因此会导致 delete p
未定义的行为。
从实用的角度来说
从技术上讲,如果没有 p = new(p) T{...}
,p
不会指向 newly-initialized 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 的 return 值分配回 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
是否为聚合类型自动在 ()
和 {}
语法之间选择来改进解决方案。