Is There a (Semantic) Difference Between the Return Value of Placement New and the Casted Value of Its Operand

Is there a (semantic) difference between the return value of placement new and the casted value of its operand?

Only a can safely be used to directly access the Foo object created by the placement new-expression (which we'll call x for ease of reference). Using b requires std::launder.

The value of a is specified in [expr.new]/1:

If the entity is a non-array object, the result of the new-expression
is a pointer to the object created.

The value of a is therefore "pointer to x". This pointer, of course, can safely be used to access x.

reinterpret_cast<Foo*>(buffer) applies the array-to-pointer conversion to buffer (see [expr.reinterpret.cast]/1). The resulting value after the conversion is applied is "pointer to the first element of buffer".
This is a reinterpret_cast of an object pointer to an object pointer of a different type, and is defined as equivalent to static_cast<Foo*>(static_cast<void*>(buffer)) by [expr.reinterpret.cast]/7.

The inner cast to void* is actually an implicit conversion. Per [conv.ptr]/2,

The pointer value is unchanged by this conversion.

Therefore the inner cast yields a void* with the value "pointer to the first element of buffer".

The outer cast is governed by [expr.static.cast]/13, which I've lightly reformatted into bullet points:

A prvalue of type “pointer to cv1 void” can be converted to a prvalue of type “pointer to cv2 T”, where T is an object type and cv2 is the same cv-qualification as, or greater cv-qualification than, cv1.

  • If the original pointer value represents the address A of a byte in memory and A does not satisfy the alignment requirement of T,
    then the resulting pointer value is unspecified.

  • Otherwise, if the original pointer value points to an object a, and there is an object b of type T (ignoring cv-qualification)
    that is pointer-interconvertible with a, the result is a pointer to
    b.

  • Otherwise, the pointer value is unchanged by the conversion.

Assuming that buffer is suitably aligned (you'd be in trouble well before this point if it's not), the first bullet is inapplicable. The second bullet is likewise inapplicable as there's no pointer-interconvertiblity here. It follows that we hit the third bullet - "the pointer value is unchanged by the conversion" and remains "pointer to the first element of buffer".

Thus, b does not point to the Foo object x; it points instead to the first char element of buffer, even though its type is Foo*. It therefore cannot be used to access x; attempting to do so yields undefined behavior (for the non-static data member case, by omission from [expr.ref]; for the non-static member function case, by [class.mfct.non-static]/2).

To recover a pointer to x from b, std::launder can be used:

b = std::launder(b); // value of b is now "pointer to x"
// and can be used to access x

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

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.

Placement new and assignment of class with const member

There is nothing that makes the shown code snippet inherently UB. However, it is almost certain UB will follow immediately under any normal usage.

From [basic.life]/8 (emphasis mine)

If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if:

  • the storage for the new object exactly overlays the storage location which the original object occupied, and

  • the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and

  • the type of the original object is not const-qualified, and, if a class type, does not contain any non-static data member whose type is const-qualified or a reference type, and

  • the original object was a most derived object of type T and the new object is a most derived object of type T (that is, they are not base class subobjects).

Since there is a const member in s, using the original variable after a call to operator= will be UB.

s var{42};
var = s{420}; // OK
do_something(var.id); // UB! Reuses s through original name
do_something(std::launder(&var)->id); // OK, this is what launder is used for

Does using non-array placement new for neighboring elements create an array?

Am I right? Can I refer to this memory as an array? If not, can this functionality be achieved somehow?

This is basically the same underlying problem that std::vector has with data() and pointed out in CWG 2182. It's undefined behavior to refer to this memory as an array, but practically speaking it needs to work because so much code relies on it working. There's no way to achieve it.


Side-node, don't call your callables lambda. They don't have to be lambdas. And also let the language help you with pointer offsetting:

auto p = static_cast<Object*>(storage);
for (int i = 0; i < size; ++i) {
construct(static_cast<void*>(p + i), i);
}

std::launder and strict aliasing rule

The strict aliasing rule is a restriction on the type of the glvalue actually used to access an object. All that matters for the purpose of that rule are a) the actual type of the object, and b) the type of the glvalue used for the access.

The intermediate casts the pointer travels through are irrelevant, as long as they preserve the pointer value. (This goes both ways; no amount of clever casts - or laundering, for that matter - will cure a strict aliasing violation.)

f is valid as long as ptr actually points to an object of type int, assuming that it accesses that object via int_ptr without further casting.

example_1 is valid as written; the reinterpret_casts do not change the pointer value.

example_2 is invalid because it gives f a pointer that doesn't actually point to an int object (it points to the out-of-lifetime first element of the storage array). See Is there a (semantic) difference between the return value of placement new and the casted value of its operand?

static_cast'd pointer value

You may misunderstand the term "pointer value". The term is defined in [basic.compound]/3:

Every value of pointer type is one of the following:

  • a pointer to an object or function (the pointer is said to point to the object or function), or

  • a pointer past the end of an object ([expr.add]), or

  • the null pointer value ([conv.ptr]) for that type, or

  • an invalid pointer value.

A value of a pointer type that is a pointer to or past the end of an object represents the address of the first byte in memory ([intro.memory]) occupied by the object or the first byte in memory after the end of the storage occupied by the object, respectively.

So you can see the term "pointer value" in the standard is a very abstract term. Even if two pointer values represent the same address, they may have different values. The example in cppreference demonstrates the concept of "pointer value" nicely:

struct S1 { int a; } s1;
struct S2 { int a; private: int b; } s2; // not standard-layout
union U { int a; double b; } u = {0};
int arr[2];

int* p1 = reinterpret_cast<int*>(&s1); // value of p1 is "pointer to s1.a" because s1.a
// and s1 are pointer-interconvertible

int* p2 = reinterpret_cast<int*>(&s2); // value of p2 is unchanged by reinterpret_cast and
// is "pointer to s2".

int* p3 = reinterpret_cast<int*>(&u); // value of p3 is "pointer to u.a": u.a and u are
// pointer-interconvertible

double* p4 = reinterpret_cast<double*>(p3); // value of p4 is "pointer to u.b": u.a and u.b
// are pointer-interconvertible because both
// are pointer-interconvertible with u

int* p5 = reinterpret_cast<int*>(&arr); // value of p5 is unchanged by reinterpret_cast and
// is "pointer to arr"


Related Topics



Leave a reply



Submit