初始化对象时可以丢弃放置新的返回值吗 [英] Is it OK to discard placement new return value when initializing objects
问题描述
此问题源自此线程中的注释部分,并且在那里也有答案.但是,我认为仅在评论部分中留白是非常重要的.所以我为此做了问答.
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,它们将作为附加参数传递到分配函数.在标准分配函数
void* operator new(std::size_t, void*)
,之后,该分配函数被称为新放置",该函数简单地返回了其第二个参数.这用于在分配的存储中构造对象[...]
这意味着new(p) vec_t{1, 2, 3}
仅返回p
,而p = new(p) vec_t{1, 2, 3}
看起来很多余.真的可以忽略返回值吗?
无论是从实践角度还是从实践角度来看,忽略返回值都是不可行的.
从学徒的角度来看
对于p = new(p) T{...}
,p
符合指向由new-expression创建的对象的指针,而new-expression对于值new(p) T{...}
并不适用,尽管其值相同.在后一种情况下,它仅符合指向已分配存储的指针的条件.
非分配全局分配函数返回其参数而没有任何隐含的副作用,但是new-expression(是否放置)始终返回指向其创建的对象的指针,即使碰巧使用了该分配函数也是如此./p>
每个cppref关于 delete-expression (强调我的)的描述:
对于第一个(非数组)形式,表达式必须是指向对象类型的指针,或者是上下文隐式可转换为此类指针的类类型,,其值必须为 null 或指向由new-expression创建的非数组对象的指针,或指向由new-expression创建的非数组对象的基础子对象的指针.如果表达式是其他任何东西,包括如果它是通过new-expression的数组形式获得的指针,则行为是不确定的.
因此,无法执行p = new(p) T{...}
会使delete p
行为不确定.
从实用的角度来看
从技术上讲,尽管值(内存地址)相同,但没有p = new(p) T{...}
的情况下,p
并不指向新初始化的T
.因此,编译器可以假定p
仍引用在放置新位置之前的T
.考虑代码
p = new(p) T{...} // (1)
...
new(p) T{...} // (2)
即使在(2)
之后,编译器仍可能假定p
仍引用在(1)
处初始化的旧值,从而进行错误的优化.例如,如果T
具有const成员,则编译器可能会将其值缓存在(1)
上,甚至在(2)
之后仍会使用它.
p = new(p) T{...}
有效地禁止了这种假设.另一种方法是使用 std::launder()
,但只需将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
是否为聚合类型,自动在()
和{}
语法之间进行选择.
This question originates from the comment section in this thread, and has also got an answer there. However, I think it is too important to be left in the comment section only. So I made this Q&A for it.
Placement new can be used to initialize objects at allocated storage, e.g.,
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
According to 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 [...]
That means new(p) vec_t{1, 2, 3}
simply returns p
, and p = new(p) vec_t{1, 2, 3}
looks redundant. Is it really OK to ignore the return value?
Ignoring the return value is not OK both pedantically and practically.
From a pedantic point of view
For p = new(p) T{...}
, p
qualifies as a pointer to an object created by a new-expression, which does not hold for new(p) T{...}
, despite the fact that the value is the same. In the latter case, it only qualifies as pointer to an allocated storage.
The non-allocating global allocation function returns its argument with no side effect implied, but a new-expression (placement or not) always returns a pointer to the object it creates, even if it happens to use that allocation function.
Per cppref's description about the delete-expression (emphasis mine):
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.
Failing to p = new(p) T{...}
therefore makes delete p
undefined behavior.
From a practical point of view
Technically, without p = new(p) T{...}
, p
does not point to the newly-initialized T
, despite the fact that the value (memory address) is the same. The compiler may therefore assume that p
still refers to the T
that was there before the placement new. Consider the code
p = new(p) T{...} // (1)
...
new(p) T{...} // (2)
Even after (2)
, the compiler may assume that p
still refers to the old value initialized at (1)
, and make incorrect optimizations thereby. For example, if T
had a const member, the compiler might cache its value at (1)
and still use it even after (2)
.
p = new(p) T{...}
effectively prohibits this assumption. Another way is to use std::launder()
, but it is easier and cleaner to just assign the return value of placement new back to p
.
Something you may do to avoid the pitfall
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)...};
}
These function templates always set the pointer internally. With std::is_aggregate
available since C++17, the solution can be improved by automatically choosing between ()
and {}
syntax based on whether T
is an aggregate type.
这篇关于初始化对象时可以丢弃放置新的返回值吗的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!