c++arraysstoragelifetimepointer-arithmetic

lifetime of an array after placement new


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?
}

LIVE

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


Solution

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

    When does the lifetime of an object end?

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