From the C++23 standard, we can determine that using placement new to create an object in storage associated with an array of std::byte
or unsigned char
does not end the lifetime of the array, as indicated by the following:
If a complete object is created (7.6.2.8) in storage associated with another object e of type “array of N unsigned char” or of type “array of N std::byte” (17.2.1), that array provides storage for the created object if:
(3.1) - the lifetime of e has begun and not ended, and
(3.2) - the storage for the new object fits entirely within e, and
(3.3) - there is no array object that satisfies these constraints nested within e.
An object a is nested within another object b if:
(4.2) - b provides storage for a
The lifetime of an object o of type T ends when:
(1.5) — the storage which the object occupies is released, or is reused by an object that is not nested within o (6.7.2).
However, I am confused by the significance of having the lifetime of the array persisting. Suppose I am creating an object pool and I use an array of char
instead. Is there some sort of downside? Can someone point out the issues that will happen without this particular rule in place?
Once you create an object within your char[N]
, the lifetime of the entire char[N]
ends. You cannot use pointer arithmetic to access parts of the array.
Ie,
char* ptr = new char[1024];
::new ( (void*)(ptr+0*sizeof(int))) int(3);
::new ( (void*)(ptr+1*sizeof(int))) int(1);
::new ( (void*)(ptr+2*sizeof(int))) int(4);
::new ( (void*)(ptr+3*sizeof(int))) int(1);
::new ( (void*)(ptr+4*sizeof(int))) int(5);
::new ( (void*)(ptr+5*sizeof(int))) int(9);
the first line hear ends the lifetime of *ptr
. All later access to it as a char[1024]
is thus illegal; there is no object that is an array of 1024 char
's there anymore.
You could allocate a new char[1024]
there!
::new( (void*)ptr ) char[1024];
but that destroys the int
(and last I checked, placement newing arrays was a bit stupidly underspecified? Maybe that has been fixed).
unsigned char* ptr = new unsigned char[1024];
::new ( (void*)(ptr+0*sizeof(int))) int(3);
::new ( (void*)(ptr+1*sizeof(int))) int(1);
::new ( (void*)(ptr+2*sizeof(int))) int(4);
::new ( (void*)(ptr+3*sizeof(int))) int(1);
::new ( (void*)(ptr+4*sizeof(int))) int(5);
::new ( (void*)(ptr+5*sizeof(int))) int(9);
The lifetime of *ptr
remains intact, and all of these operations appear legal (well, up to alignment issues).
I am unaware of a compiler that enforces this difference. But part of the point of these changes it so make bit fiddling work along side strong aliasing rules; strong aliasing rules allow a compiler to make assumptions that fiddling with a char*
won't modify an int const&
without having to check addresses.
The lifetime of an object o of type T ends when: [SNIP] (2.5) the storage which the object occupies is released, or is reused by an object that is not nested within o ([intro.object]). The properties ascribed to objects and references throughout this document apply for a given object or reference only during its lifetime.
So what can you do with that object?
Before the lifetime of an object has started but after the storage which the object will occupy has been allocated21 or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that represents the address of the storage location where the object will be or was located may be used but only in limited ways. For an object under construction or destruction, see [class.cdtor]. Otherwise, such a pointer refers to allocated storage ([basic.stc.dynamic.allocation]), and using the pointer as if the pointer were of type
void*
is well-defined. Indirection through such a pointer is permitted but the resulting lvalue may only be used in limited ways, as described below. The program has undefined behavior if
[SNIP]
the pointer is used as the operand of a static_cast ([expr.static.cast]), except when the conversion is to pointer to cv void, or to pointer to cv void and subsequently to pointer to cv char, cv unsigned char, or cv std::byte ([cstddef.syn])
[SNIP]
So you can take a pointer to the char[1024]
whose initial memory was reused (thus ending the liftime of the char[1024]
array), treat it as a void*
, cast it to a char*
(via static_cast
, which C-style casting will often do).
Now, here is the trap. What happens when we do ptr[22]
or ptr+22
?
Well, that is covered by [expr.add]/4:
When an expression J that has integral type is added to or subtracted from an expression P of pointer type, the result has the type of P.
(4.1) If P evaluates to a null pointer value and J evaluates to 0, the result is a null pointer value. (4.2) Otherwise, if P points to a (possibly-hypothetical) array element i of an array object x with n elements ([dcl.array]),64 the expressions P + J and J + P (where J has the value j) point to the (possibly-hypothetical) array element i+j of x if 0≤i+j≤n and the expression P - J points to the (possibly-hypothetical) array element i−j of x if 0≤i−j≤n. (4.3) Otherwise, the behavior is undefined.
Well, ptr
is not a null pointer. So (4.1) does not apply. The lifetime of the object char[1024]
has ended, ptr
does not point to an array object, so (4.2) does not apply. Thus, (4.3) applies; the result is undefined.