c++memory-managementlanguage-lawyerlifetimeplacement-new

placement new and storage reuse


In several location within the standard, storage reuse is mentioned, such as here: https://timsong-cpp.github.io/cppwp/n4861/basic.life#1.5.

But I cannot get a proper definition of what is a storage reuse vs what would be an incorrect object creation. I'll illustrate that with the following snippet:

#include <cstddef>
#include <cstdint>
#include <new>

int main() {
    std::byte storage[10000];
    std::int32_t *po0 = new (storage + 2 * sizeof(std::int32_t)) std::int32_t;
    // does this "reuse" po0 storage?
    std::int16_t *po1 = new (storage + 2 * sizeof(std::int32_t)) std::int16_t;
    // std::int64_t *po2 = new (storage + 2 * sizeof(std::int32_t)) std::int64_t;
    // std::int16_t *po3 =
    //     new (storage + 2 * sizeof(std::int32_t) + sizeof(std::int8_t))
    //         std::int16_t;
    // std::int32_t *po4 =
    //     new (storage + 2 * sizeof(std::int32_t) + sizeof(std::int32_t))
    //         std::int32_t;
    // std::int32_t *po5 =
    //     new (storage + 2 * sizeof(std::int32_t) - sizeof(std::int16_t))
    //         std::int32_t;
    // std::int64_t *po6 =
    //     new (storage + 2 * sizeof(std::int32_t) - sizeof(std::int16_t))
    //         std::int64_t;
}

LIVE

NB for simplicity sake, I didn't consider alignment issues but only where the new object creation is occurring (before the first object, at same location, inside the object) and how the new object is overlapping the first one.
NB I used integers for the example, but it can be any type.
NB consider that only one of the six objects *po1 to *po6 is created.

Which of these 6 object creation by placement new are valid and constitute a storage reuse, that will end *po0 lifetime?

Here is a graphical representation of the different scenarii:

po0 po0 po0 po0
po1 po1
po2 po2 po2 po2 po2 po2 po2 po2
po3 po3
po4 po4 po4 po4
po5 po5 po5 po5
po6 po6 po6 po6 po6 po6 po6 po6

Solution

  • The standard says very little about what it means to reuse storage, but [basic.life]/2 does say at the end

    [...] When evaluating a new-expression, storage is considered reused after it is returned from the allocation function, but before the evaluation of the new-initializer ([expr.new]).

    and we also have the following note in [intro.object]/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 array provides storage for the created object if

    • the lifetime of e has begun and not ended, and
    • the storage for the new object fits entirely within e, and
    • there is no array object that satisfies these constraints nested within e.

    [...] [Note 3: If that portion of the array previously provided storage for another object, the lifetime of that object ends because its storage was reused ([basic.life]). — end note]

    Now [basic.life]/2 doesn't say exactly what region of storage is considered reused. An allocation function returns only a void*; where does the block of reused storage end? Presumably, it's the block of storage starting at the address represented by the void* return value, and containing a number of bytes equal to the first argument that was passed to the allocation function. (It can't be just the block of storage used by the object that is created by the new-expression, because if the new-expression needs to store any metadata in the block, e.g. the number of destructors that delete[] should call, then obviously the part of the storage used by the metadata is also getting reused.)

    [intro.object]/3 makes it clear that you can create an object (using a new-expression) in a std::byte buffer in storage that is currently being occupied by some other object that the buffer provides storage for. The result is storage is considered reused. Not the entire buffer; only "that portion of the array", i.e., the part occupied by the newly created object.

    And finally, I hope it is obvious that whenever any part of the storage of a scalar object is reused, the entire scalar object's lifetime ends. How can it still be alive if part of its storage is being used by a more recently created object?

    So, assuming that the alignment is correct, creating any of the objects pointed to by po1 through po6 ends the lifetime of the object pointed to by po0.

    The above points would be worth clarifying in the standard, but I think there's no genuine ambiguity about the behavior of your example. On the other hand, the standard doesn't explain what happens if e.g. you reuse only part of a class object's storage; do class members that are disjoint from the reused region of storage stay alive? (It is possible for non-static data members to be alive while the enclosing class is not; this occurs during ordinary destruction, since as soon as the destructor starts, the class object's lifetime is considered to have ended.)