This is a follow-up on this question and my answer
#include <new>
struct A {
int x{42};
int y{24};
};
static_assert(sizeof(A) == 2 * sizeof(int));
static_assert(alignof(A) == alignof(int));
int main() {
int storage[5] = {0, 1, 2, 3, 4};
::new (static_cast<void *>(storage + 1)) A;
// what's the lifetime status of storage and its subobjects?
// is pointer arithmetic still valid?
return *(storage + 4); // still OK?
}
After reusing a the storage of some storage
objects through placement new, is storage
array still in its lifetime, pointer arithmetic valid and dereferencing valid?
Even if I'm reusing several time the same locations?
the rule for "providing storage" (https://en.cppreference.com/w/cpp/language/lifetime and https://timsong-cpp.github.io/cppwp/n4861/basic.memobj#intro.object-3) seems to explicitly make an exception for arrays of std::bytes and unsigned char, thus I'm unsure of what happen for other types of array.
Regarding dereferencing of the parts overlayed by A
, I give an argument in my linked answer, using the notion of transparently replacable
which would not apply if, instead of A
I have used, for instance, a double
(alignment issue left aside).
A slight modification to your code, so that the ::new
object can be named *pA
. Other than the name this has no impact on the logic that follows:
int storage[5] = {0, 1, 2, 3, 4};
A* pA = ::new (static_cast<void *>(storage + 1)) A;
[intro.object] 6.7.2/4 reads:
4 An object a is nested within another object b if:
(4.1) a is a subobject of b, or
(4.2) b provides storage for a, or
(4.3) there exists an object c where a is nested within c, and c is nested within b.
This is not true of *pA
and storage
; *pA
is not "nested within" storage
, because "provides storage for" requires:
[intro.object] 6.7.2/3:
If a complete object is created ([expr.new]) in storage associated with another object e of type “array of N unsigned char” or of type “array of N std::byte” ([cstddef.syn])[...]
that the array be of type unsigned char
or std::byte
; here you chose int
.
[basic.life] 6.7.3/1.2 reads:
The lifetime of an object o of type T ends when:
(1.3) if T is a non-class type, the object is destroyed, or
(1.4) if T is a class type, the destructor call starts, or
(1.5) the storage which the object occupies is released, or is reused by an object that is not nested within o ([intro.object]).
So when you call *pA = ::new
, you are reusing the storage that storage
occupies. You are not nested-within, so you end the life of storage
.
The pointer arithmetic on storage
is no longer valid, as the properties of objects (like "being an array" and "pointer arithmetic is valid") don't exist when the objects lifetime ends unless explicitly permitted.
The next question is, storage
then goes out of scope. Are we screwed?
[basic.life] 6.7.3/9 reads:
If a program ends the lifetime of an object of type T with static, thread, or automatic storage duration and if T has a non-trivial destructor,30 the program must ensure that an object of the original type occupies that same storage location when the implicit destructor call takes place; otherwise the behavior of the program is undefined. This is true even if the block is exited with an exception.
As it happens, an object of type int[5]
has a trivial destructor. So at the end of storage
scope we don't cause your program to be ill-formed.
You can fix your problem by making storage
be an array of unsigned char
or std::byte
, and fiddling with your pointer arithmetic a bit.