Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it OK to discard placement new return value when initializing objects

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?

like image 452
Lingxi Avatar asked Mar 30 '18 05:03

Lingxi


People also ask

Why use placement new?

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.

In which cases we would need to use placement new?

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.

Does placement New allocate memory?

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.

Does vector use placement new?

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".


1 Answers

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.

like image 134
Lingxi Avatar answered Oct 16 '22 11:10

Lingxi