First I need to note that I have asked a similar question several times, and last time I have got an almost satisfactory answer. However, the wording of the c++20 standard (draft) is not clear to me, and I am curious whether it is a defect within the standard or in the comprehension of the standard.
C++20 standard introduced the notion of implicit-lifetime types.
Scalar types, implicit-lifetime class types ([class.prop]), array types, and cv-qualified versions of these types are collectively called implicit-lifetime types.
Further the standard (draft) says https://eel.is/c++draft/basic.memobj#intro.object-10
Some operations are described as implicitly creating objects within a specified region of storage. For each operation that is specified as implicitly creating objects, that operation implicitly creates and starts the lifetime of zero or more objects of implicit-lifetime types ([basic.types]) in its specified region of storage if doing so would result in the program having defined behavior. If no such set of objects would give the program defined behavior, the behavior of the program is undefined. If multiple such sets of objects would give the program defined behavior, it is unspecified which such set of objects is created.
[Note 3: Such operations do not start the lifetimes of subobjects of such objects that are not themselves of implicit-lifetime types.— end note]
and https://eel.is/c++draft/basic.memobj#intro.object-11.
Further, after implicitly creating objects within a specified region of storage, some operations are described as producing a pointer to a suitable created object. These operations select one of the implicitly-created objects whose address is the address of the start of the region of storage, and produce a pointer value that points to that object, if that value would result in the program having defined behavior. If no such pointer value would give the program defined behavior, the behavior of the program is undefined.
If multiple such pointer values would give the program defined behavior, it is unspecified which such pointer value is produced.
The paper P0593R6 Implicit creation of objects for low-level object manipulation provides motivation and more examples for these.
One of the motivations for introduction of implicit-lifetime objects into standard was dynamic construction of arrays in order to make the pointer arithmetic in the example non-UB.
One of the proposed changes to the standard 5.9. 16.5.3.5 Table 34: Cpp17Allocator requirements, (which is the same as Example 1 in allocator requirements ) gives the following example.
Example 1: When reusing storage denoted by some pointer value p,
launder(reinterpret_cast<T*>(new (p)byte[n * sizeof(T)]))
can be used to implicitly create a suitable array object and obtain a pointer to it.
Which basically implies that the idea is to make the syntax (T*)malloc(...)
and (T*)(::operator new(... ) )
(and similar cases) valid for subsequent pointer arithmetic.
I also understand the idea of "superposition" - that the suitable created object to which we return the pointer is determined on the usage, as long as an object that allows to use that pointer "legally" exists, we are fine.
Hence, I believe that the following snippets are fine
int* numbers = (int*)::operator new(n * sizeof(int) );
// Fine. Operator new implicitly creates an int array of a size smaller than `n` that is determined later
// and returns a pointer to the first element Both byte and byte array are implicitly constructed, so no
// problem here.
// the pointer to suitable created object points at numbers[0] which is implicitly constructed, so no
// problem here
A slightly modified example from the cited paper..
alignof(int) alignof(float) char buffer[max(sizeof(int), sizeof(float))];
*(int*)buffer = 5;
// First usage implicitly created an int and we have a pointer to that int, (int*)buffer is the pointer
// to suitable created object.
new (buffer) std::byte[sizeof(buffer)];
// ends the lifetime of the int and implicitly creates a float instead of
// it since next usage (float*) buffer implies implicit float creation
*(float*)buffer = 2.0f; // (float*)buffer is the pointer to the suitable created object (float) that was
// implicitly created by previous statement
However, note that we did not have a problem yet - all the types are implicitly constructed, so returning a pointer to first element of the array is fine - it is has implicitly constructed itself.
But what do we do to in order to create an array of type T
if T
is not an implicit-lifetime type?
Well, that should not be a problem.
auto memory = ::operator new(sizeof(T) * n, std::alignval_t{alignof(T)});
// implicitly creates a T array of size n.
auto arr_ptr = reinterpret_cast<T(*)[n]>(memory); // returns a pointer to that array
auto arr = *arr_ptr; // dereferences the pointer to our implicitly constructed array, and the result
// should be valid
However, there is a problem with this construct.
- First of all - one of the motivations in the proposal paper was to make the usage of casting to
T*
valid, not toT(*)[]
orT(*)[n]
. - The examples in the standard and the proposal paper suggest that cast to
T*
is a valid usage. Let's look at the following snippet.
T* arr = (T*)::operator new(sizeof(T) * n, std::align_val_t{alignof(T)});
Now we have a problem. arr
points at an object that is not constructed. No matter how we look at it, no matter what the size of the implicitly constructed T array is, its first element is not an implicit-lifetime object and therefore a pointer value that points at arr[0] is not a pointer to one of the implicitly-created objects whose address is the address of the start of the region of storage, hence the requirement for a pointer to suitable created object is not satisfied. I believe that same reasoning makes the example in allocator.requirements invalid. More than that - if n
in example is replaced with 1
, we basically produce a pointer to T
object of non implicit-lifetime type (it is not even array, but just a pointer to T
that has not yet been constructed) which is just plain wrong.
So where is the problem? Is the example indeed invalid? But then, the example in proposal paper is still invalid (UB) and making it valid (not UB), was one the motivations for introduction of implicit-lifetime into the standard.
So, how is this conflict solved? Is there an "error" in the wording in the standard and a pointer to suitable created object does not actually have to point to an object, but any valid pointer value that results in defined behavior is fine (and a pointer to an array element that has not yet begun its lifetime is a valid value for pointer arithmetic as far as I am aware, and pointer arithmetic is want we want here, after all) or pointer to first element before beginning of its lifetime a suitable created object (despite the fact that does not have implicit-lifetime) for reasons I do not understand? Or the examples in the motivation and allocator.requirements snippet are wrong, and despite being one of the main motivations for introduction of implicit-lifetime into the standard, they are still UB? Something else?
Diclaimer: Language-lawyer type of question.
An explanation in the answer to the cited question is practically fine. Besides that we have the standard library allocator facilities that allow creation of uninitialized arrays which should cover most of cases. After all, casting to (T*)
(along with some laundering may be) work in practice, and the intention of the proposal paper to make it work. I believe that is the intention of the standard authors as well. This a question is how does the standard resolve this contradiction.
PS:
I do not mind if the answer to this question is merged with the cited one (as long as my question here gets answered), but I do believe that these questions are different - that one asked how to create an array without initializing its elements and this one asks about something that is (I believe so at least) an ambiguity (or contradiction) in the c++20 standard (well draft of it) and how to resolve it.