问题
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?
回答1:
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.
来源:https://stackoverflow.com/questions/49568858/is-it-ok-to-discard-placement-new-return-value-when-initializing-objects