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;
}
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 |
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 Nstd::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.)