General context
The general use-case is the following.
We have a storage of std::byte or another type that can provide storage.
Within this storage, we create an object (or many) of some type T at address ptr, which respect T alignement.
Many questions have already been posted regarding the correct way to retrieve the object without being UB and they are generally those two different answers:
1- T * pt = reinterpret_cast
2- T * pt = std::launder(reinterpret_cast
I was personally strongly in favor of the second version and wrote several answer using this techniques, following what I understood from Jonathan Müller talk about lifetime (see below).
But many experienced contributor claimed that the call to std::launder was useless (though it does, at most, slightly impact performances but does not introduce an incorrect behavior). For instance, this question with no accepted answer to this date is the perfect illustration of the debate: Using std::launder with reinterpret_cast?.
Though the present question is close to many other one about reinterpret_cast and std::launder I hope to present the issue under a different light, in order to get a definitive, argued, answer.
The standard
So I dived (again) into the standard definition of std::launder` in ptr.launder:
Preconditions: p represents the address A of a byte in memory. An object X whose type is similar ([conv.qual]) to T is located at the address A, and is either within its lifetime ([basic.life]) or is an array element subobject whose containing array object is within its lifetime. All bytes of storage that would be reachable through ([basic.compound]) the result are reachable through p.
...
Returns: A value of type T* that points to X.
...
[Note 1: If a new object is created in storage occupied by an existing object of the same type, a pointer to the original object can be used to refer to the new object unless its complete object is a const object or it is a base class subobject; in the latter cases, this function can be used to obtain a usable pointer to the new object. See [basic.life]. — end note]
(I have only kept the passages that seemed relevant to this question)
The use-cases
Now let's examine several situations:
reusing an old pointer after recreating a similar object at the same location:
void non_const_reuse() {
alignas(T) std::byte storage[10 * sizeof(T)];
new (static_cast(storage + 3 * sizeof(T))) T();
T* pt = reinterpret_cast(storage + 3 * sizeof(T));
T* pt_tmp = pt;
pt_tmp->~T();
new (static_cast(storage + 3 * sizeof(T))) T();
[[maybe_unused]] int value = pt->data;
}
Note 1 above does not apply because the constructed object is neither const nor a subobject of a base class. So std::launder is not required anywhere.
reusing an old pointer on const object, after recreating a similar object at the same location:
void const_reuse() {
alignas(T) std::byte storage[10 * sizeof(T)];
new (static_cast(storage + 3 * sizeof(T))) const T();
T const* pt = reinterpret_cast(storage + 3 * sizeof(T));
// note: no explicit destruction
new (static_cast(storage + 3 * sizeof(T))) T();
// [[maybe_unused]] const int value =
// pt->data; // UB, compiler assumes pt points to constant object, thus is
// // unchanged
[[maybe_unused]] const int value =
std::launder(pt)->data; // not UB, compiler is told the object may be different than before
}
This time, the object pointed by pt was supposed to be const but its lifetime ended at the placement new and it is now a new const object and the compiler must be told so with std::launder, otherwise he could assume that the object has not changed.
Retrieving a pointer to an object created inside a storage:
Here is the use-case that interests me most and seems to be the source of most debates:
#include // std::byte
#include // placement new
struct T {
int data = 42;
};
void placement_new_no_reuse() {
alignas(T) std::byte storage[10 * sizeof(T)];
new (static_cast(storage + 3 * sizeof(T))) T();
[[maybe_unused]] T* pt = reinterpret_cast(storage + 3 * sizeof(T)); // SHOULD I USE STD::LAUNDER?
}
int main() { placement_new(); }
Here I have a single new object and Note 1 above does not apply, thus std::launder shouldn't be necessary.
Yet I was impressed by Jonathan Müller speech about lifetime (https://www.jonathanmueller.dev/talk/lifetime/) and he told that, when re-using memory as storage for a different type (see slide 82) std::launder should be used also and I sticked to this recommandation:
T* pt = std::launder(reinterpret_cast(storage + 3 * sizeof(T)));`
But I must admit that it does not seem to fit with Note 1 above (noticeably, we don't reuse a pointer here). Yet he even made a proposal in order to reduce the supposed need for std::launder (P3006)
The question
So is Müller right and how does it fit with the standard?