3

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.

  1. First of all - one of the motivations in the proposal paper was to make the usage of casting to T* valid, not to T(*)[] or T(*)[n].
  2. 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.

  • "*But what do we do to in order to create an array of type T if T is not an implicit-lifetime type?*" Are you talking about an array of actual `T`s that exist, or just an array object of type `T[]`? – Nicol Bolas Mar 22 '21 at 23:47
  • I want to create an object of type `T[]`. – Myrddin Krustowski Mar 22 '21 at 23:49
  • Then it's the same question. You don't want to create the `T`s, you want to create the array around some hypothetical `T`s. There's no difference. – Nicol Bolas Mar 22 '21 at 23:50
  • The difference here is that there is ambiguity in the standard. I need `pointer to suitable created object` which if `T` does not have implicit lifetime, the result of `reinterpret_cast` is not, and it doesn't matter if I construct it later. On the other hand, if any valid pointer works, and it doesn't have to point to an object is fine, then the standard doesn't say it. – Myrddin Krustowski Mar 22 '21 at 23:52
  • It doesn't need to be. Just get a pointer to an element in the array. That gives you 100% of the information you need to get on with whatever you need to get on with. – Nicol Bolas Mar 22 '21 at 23:53
  • That's not what the standard say, however, and that's why I asked this question. It explicitly says a **pointer to an object**. Is it a defect in the standard? – Myrddin Krustowski Mar 22 '21 at 23:56
  • How is the pointer to `T` not an object? Are you confused by the fact that the object's lifetime hasn't begun? – Nicol Bolas Mar 22 '21 at 23:58
  • No. I am confused by the fact **that it is not an implicitly created object**, which the standard requires. And lifetime thing is problematic as well. The idea is that we have superposition, but the object of implicit lifetime is created during allocation. This is not the case. – Myrddin Krustowski Mar 23 '21 at 00:02
  • https://eel.is/c++draft/basic.memobj#intro.object-11. See yourself :-) The requirements are not satisfied. I understand the idea and the intention, but the wording is problematic. – Myrddin Krustowski Mar 23 '21 at 00:04
  • BTW, I think that *created* means began its lifetime here, but feel free to correct me if I am wrong. – Myrddin Krustowski Mar 23 '21 at 00:09
  • _A slightly modified example from the cited paper_ I think you misunderstood. The paper says that the examples in that section are not valid (and won't/didn't become valid). _First usage implicitly created an int_ This is incorrect. Usage doesn't create objects. _`(float*)buffer` is the pointer to the suitable created object_ No, a `char` object and a `float` object are not pointer-interconvertible, you can't get a pointer to one from a pointer to another using `reinterpret_cast`. – Language Lawyer Mar 23 '21 at 01:11
  • _But then, the example in proposal paper is still invalid (UB)_ I don't think this paper sets a goal to make that example valid as it is written. Implementor of a `vector`-like class is expected to use `std::launder` in appropriate places (e.g. in `begin` or `operator[]`). The issue was that, even using `std::launder`, one couldn't implement a `vector`-like class w/o implicit arrays creation. I think `std::launder` is in details which were omitted for brevity. – Language Lawyer Mar 23 '21 at 01:21
  • _I think that created means began its lifetime here_ No, creating object ≠ starting its lifetime. Link you posted above even says _that operation implicitly creates **and** starts the lifetime_. So they are 2 different actions. – Language Lawyer Mar 23 '21 at 01:25
  • _I believe that same reasoning makes the example in allocator.requirements invalid._ I think you are right here. If `T` is not an implicit-lifetime type, one can't use `std::launder` like it is used there, it violates [`std::launder` Preconditions](https://timsong-cpp.github.io/cppwp/n4861/ptr.launder#2). Try to file an editorial issue first. (If `T` is an implicit-lifetime type, `std::launder` is just not necessary). – Language Lawyer Mar 23 '21 at 01:39
  • https://github.com/cplusplus/draft/issues/4553 – Language Lawyer Mar 23 '21 at 02:48
  • @LanguageLawyer Of course, you are right. I ignored the laundering part (wrongly, I admit) - I did change the example (its purpose was to show that bits of destroyed object can shall not be reused) - I changed it a bit, just to demonstrate implicit creation of the array. Creation ≠ lifetime, but in paragraphs I have mentioned, it implies that creation and beginning of the lifetime. Creation is not mentioned in 11 because it is mentioned in 10. – Myrddin Krustowski Mar 23 '21 at 07:18
  • @LanguageLawyer Same for the vector example - the motivation is to make result of casting to (T*) viable for pointer arithmetic, but the wording in the standard is problematic. What we need is to state explicitly that pointers to array elements are valid pointers as long as you do not dereference them and use them solely for the purpose of arithmetic within the storage occupied by the array. We should also allow to launder them if they are used for arithmetic. – Myrddin Krustowski Mar 23 '21 at 07:23
  • @LanguageLawyer *"I don't think this paper sets a goal to make that example valid as it is written"* - not the example itself, but the pattern of converting pointer to allocated storage to `T*` and using the resulting pointer as a pointer to array member. The example still does not work, even if we ignore the need for laundering. – Myrddin Krustowski Mar 23 '21 at 07:25
  • I think that _These operations select one of the implicitly-created objects_ can select not only the whole implicitly-created array, but also its first element, and produce a pointer to it, even when `T` is not an implicit-lifetime type. – Language Lawyer Mar 23 '21 at 12:56
  • @LanguageLawyer It is obvious, that it is the intent of the standard (at least in the case of arrays) However, I can't deduce it from the wording. Elements of an array (or at least first element) should be legal objects for these purposes, provided that the array exists (laundering as well), otherwise we still have UB. – Myrddin Krustowski Mar 26 '21 at 06:58

0 Answers0