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?
Placement new is used when you do not want operator new to allocate memory (you have pre-allocated it and you want to place the object there), but you do want the object to be constructed.
Placement new allows you to construct an object in memory that's already allocated. You may want to do this for optimization when you need to construct multiple instances of an object, and it is faster not to re-allocate memory each time you need a new instance.
Placement new is a variation new operator in C++. Normal new operator does two things : (1) Allocates memory (2) Constructs an object in allocated memory. Placement new allows us to separate above two things. In placement new, we can pass a preallocated memory and construct an object in the passed memory.
With std::vector , a memory buffer of the appropriate size is allocated without any constructor calls. Then objects are constructed in place inside this buffer using "placement new".
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With