cplusplus / CWG

Core Working Group
23 stars 7 forks source link

[basic.life] p8 Whether the pointer that points to an element atomically points to the new created object? #452

Open xmh0511 opened 8 months ago

xmh0511 commented 8 months ago

Full name of submitter (unless configured in github; will be published with the issue): Jim X

Consider this example:

struct A{
   A(){}
  ~A(){}
  int a = 0
};
int main(){
   //auto ptr = (A*)malloc(sizeof(A) * 10);  
   auto ptr = (A*)*(T(*)[10])malloc(sizeof(A) * 10);  // #1 or simply write std::start_lifetime_as_array<A>(malloc(sizeof(A) * 10), 10);
   auto third_element_ptr =   ptr + 2;
   new (third_element_ptr) A{};  // #2
   auto ele = ptr + 2;
   ele->a = 1; // #3 UB or not? 
}

The malloc at #2 can be considered to implicitly create an object of type array of 10 A but these subobjects are not because type A is not implicit-lifetime type, and ptr is considered to point to the first element of the array, then we create an object of type A at the storage of the element. However, is #3 UB or not? According to [basic.life] p8

If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if the original object is transparently replaceable (see below) by the new object.

The third_element_ptr does not point to an object anyhow because there is no object created at the storage pointed by third_element_ptr at all.

Suggested Resolution

Is std::launder necessary here to obtain the pointer ele to point to the created object? Or, should [basic.life] p8 also cover this case?

frederick-vs-ja commented 8 months ago

Looks like a duplicate of #378.

I believe currently everything will be fine (modulo allocation failure) if we replace #1 with auto ptr = std::start_lifetime_as_array<A>(malloc(sizeof(A) * 10), 10);.

std::launder should be unrelated (at this moment), although it may be desired if the result of std::launder can point to a non-living element of a living array.

xmh0511 commented 8 months ago

Looks like a duplicate of #378.

I believe currently everything will be fine (modulo allocation failure) if we replace #1 with auto ptr = std::start_lifetime_as_array<A>(malloc(sizeof(A) * 10), 10);.

std::launder should be unrelated (at this moment), although it may be desired if the result of std::launder can point to a non-living element of a living array.

Anyway, there is no object of the type T at the slot of the element of the array. So, [basic.life] p8 does not cover this case, it is unclear whether the access is fine.

I meant whether std::launder is required to obtain the pointer ele that points to the created object?

languagelawyer commented 8 months ago

ptr is considered to point to the first element of the array

Not really, because we can only produce pointers to implicit-lifetime objects https://timsong-cpp.github.io/cppwp/n4868/intro.object#11.sentence-2. I've pointed at this long time ago (don't remember the editorial issue/PR), and told that this should be fixed to include subobjects of implicitly-created objects

The third_element_ptr does not point to an object anyhow because there is no object created at the storage pointed by third_element_ptr at all.

I do not see the connection. The array object is created, and haz subobjects, so third_element_ptr can point to it.

As I noted somewhere recently, it is not about [basic.life]/8, but more about [expr.add]/4, which should specify which array element the arithmetic should produce a pointer to, because there can be many of them stacked on top of each other.

Anyway, there is no object of the type T at the slot of the element of the array.

There is an object, not within lifetime.

frederick-vs-ja commented 8 months ago

I meant whether std::launder is required to obtain the pointer ele that points to the created object?

I don't think it's required.

IIUC the major problem is whether ptr properly points to the first element of the array:

  1. Given A is not an implicit-lifetime type, the result of malloc can point to the implicitly created array object, but not its first element object.
  2. An array and its first element are not pointer-interconvertible, so casting the returned pointer to A* doesn't produce a pointer to the first element. std::launder doesn't help, because the result of std::launder is required to point to a living object of a similar type.
  3. As a result, (A*)malloc(sizeof(A) * 10) doesn't point to an array element, and ptr + 2 has undefined behavior.

std::start_lifetime_as_array can make these stuffs well-defined. But I wonder whether it should be necessary.

xmh0511 commented 8 months ago

Not really, because we can only produce pointers to implicit-lifetime objects https://timsong-cpp.github.io/cppwp/n4868/intro.object#11.sentence-2. I've pointed at this long time ago (don't remember the editorial issue/PR), and told that this should be fixed to include subobjects of implicitly-created objects

Maybe, I need to change #1 to auto ptr = (A*)*(T(*)[10])malloc(sizeof(A) * 10);, to make the array-to-pointer conversion apply here to make ptr points to the first element.

There is an object, not within lifetime.

Where is the object from? The operation does not implicitly create the subobject because A is not an implicit-lifetime type. Who creates these subobjects?

xmh0511 commented 8 months ago

I meant whether std::launder is required to obtain the pointer ele that points to the created object?

I don't think it's required.

IIUC the major problem is whether ptr properly points to the first element of the array:

1. Given `A` is not an implicit-lifetime type, the result of `malloc` can point to the implicitly created array object, but not its first element object.

2. An array and its first element are not pointer-interconvertible, so casting the returned pointer to `A*` doesn't produce a pointer to the first element. `std::launder` doesn't help, because the result of `std::launder` is required to point to a living object of a similar type.

3. As a result, `(A*)malloc(sizeof(A) * 10)` doesn't point to an array element, and `ptr + 2` has undefined behavior.

std::start_lifetime_as_array can make these stuffs well-defined. But I wonder whether it should be necessary.

Yes, the example in this question is not my intend, see my above comment. I try to update the source code in my question to eliminate the error.

languagelawyer commented 8 months ago

Where is the object from?

From array creation

Who creates these subobjects?

You don't need to create subobjects for them to exist, creating a top-level object is enough. See the wording for new T (and, AFAIR, also declarations T x), it says that the object of type T is created. And that's all. But we always considered subobjects to exist after new T or T x. The conclusion is that subobjects need not be «created» to exist.

xmh0511 commented 8 months ago

Where is the object from?

From array creation

Who creates these subobjects?

You don't need to create subobjects for them to exist, creating a top-level object is enough. See the wording for new T (and, AFAIR, also declarations T x), it says that the object of type T is created. And that's all. But we always considered subobjects to exist after new T or T x. The conclusion is that subobjects need not be «created» to exist.

A subobject is also called an object. intro.object#1 says

The properties of an object are determined when the object is created.

If we didn't create that object, we cannot have the properties an object has.

languagelawyer commented 8 months ago

The properties of an object are determined when the object is created.

Ok, and we have [dcl.array]/6 property

An object of type “array of N U” consists of a contiguously allocated non-empty set of N subobjects of type U, known as the elements of the array, and numbered 0 to N-1.

So how is it possible to create an array object with no elements?

xmh0511 commented 8 months ago

The properties of an object are determined when the object is created.

Ok, and we have [dcl.array]/6 property

An object of type “array of N U” consists of a contiguously allocated non-empty set of N subobjects of type U, known as the elements of the array, and numbered 0 to N-1.

So how is it possible to create an array object with no elements?

This sounds like this case

struct B{int b;};
int main(){
   B obj;
  using I = int;
   obj.b.~I();
}

Does the containing object obj exist after we explicitly destroy its subobject? I think element or member looks more like a slot. If there is an object created at that slot and satisfies [intro.object] p2, then we call it the subobject of the complete object.

So, I prefer to divorce the element or member from subobjects. Moreover, carefully read [basic.life] p6

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.

We say the pointer points to storage instead of saying it points to an object, because, there is no object exists.

languagelawyer commented 8 months ago

Does the containing object obj exist after we explicitly destroy its subobject?

Why not? There is still a subobject corresponding to B::b NDSM, just out of lifetime

xmh0511 commented 8 months ago

Does the containing object obj exist after we explicitly destroy its subobject?

Why not? There is still a subobject corresponding to B::b NDSM, just out of lifetime

B::b is dead, which means, it does not have a lifetime, we cannot call it an object anymore. The sufficient condition for being called an object is whether it has a lifetime, this is confirmed in https://github.com/cplusplus/draft/issues/4921#issuecomment-922896323, the comment says an entity can be called an object if it has a lifetime.

languagelawyer commented 8 months ago

B::b is dead, which means, it does not have a lifetime

No idea how you manage to come to such conclusions

t3nsor commented 8 months ago

Does the containing object obj exist after we explicitly destroy its subobject?

Why not? There is still a subobject corresponding to B::b NDSM, just out of lifetime

B::b is dead, which means, it does not have a lifetime, we cannot call it an object anymore. The sufficient condition for being called an object is whether it has a lifetime, this is confirmed in cplusplus/draft#4921 (comment), the comment says an entity can be called an object if it has a lifetime.

Objects always have lifetime. It's just that they're not always within their lifetime.

The problem is that the wording used in the standard doesn't always reflect the "consensus framework" (and there's a lot of work that needs to be done to fix it).

sergey-anisimov-dev commented 8 months ago

Similar issues raised in #416; I would again like to state that the object model seems underspecified and contradictive on attempts to rely on implicits.

So, I prefer to divorce the element or member from subobjects.

Yes, problems seem to stem from interchangeable use of "subobjects" in a literal sense and while describing ways of referring to them, e.g. non-static data member names: those are orthogonal things. As do they apparently stem from a model in which objects normatively indefinitely "leak"/"go rogue".

xmh0511 commented 8 months ago

I think the consensus should be, that when we say an object or a subobject, we intend to mean the object is alive at the storage the object occupies now, otherwise, we can only say a pointer, a name refers to a storage, element, member slot or something else but definitely not an object. If it were not that, we would infinitely go back if there were thousands of objects that used to alive in that storage when we would talk about the object.

t3nsor commented 8 months ago

But that's true. You can dynamically allocate a block of memory and construct a const object into it. Take its address and store it somewhere. Then destroy the object and create a new one and repeat this many times. Now you have a bunch of dangling pointers that point at the ghosts of departed objects and need to be laundered in order to point to the currently alive object.

xmh0511 commented 8 months ago

But that's true. You can dynamically allocate a block of memory and construct a const object into it. Take its address and store it somewhere. Then destroy the object and create a new one and repeat this many times. Now you have a bunch of dangling pointers that point at the ghosts of departed objects and need to be laundered in order to point to the currently alive object.

So, you meant std::launder must be necessary in the following case?

int s = 0;
using T = int;
s.~T();
new (&s) int;
auto ptr = std::launder(&s);

s cannot refer to the newly created object.

frederick-vs-ja commented 8 months ago

But that's true. You can dynamically allocate a block of memory and construct a const object into it. Take its address and store it somewhere. Then destroy the object and create a new one and repeat this many times. Now you have a bunch of dangling pointers that point at the ghosts of departed objects and need to be laundered in order to point to the currently alive object.

So, you meant std::launder must be necessary in the following case?

int s = 0;
using T = int;
s.~T();
new (&s) int;
auto ptr = std::launder(&s);

s cannot refer to the newly created object.

I think he meant that the object was originally created by something like new const int(0).

Note that the storage is reusable ([basic.life] p10), but the rules of transparent replaceability (currently) don't specially treat such const objects ([basic.life] p8.3).

xmh0511 commented 8 months ago

But that's true. You can dynamically allocate a block of memory and construct a const object into it. Take its address and store it somewhere. Then destroy the object and create a new one and repeat this many times. Now you have a bunch of dangling pointers that point at the ghosts of departed objects and need to be laundered in order to point to the currently alive object.

So, you meant std::launder must be necessary in the following case?

int s = 0;
using T = int;
s.~T();
new (&s) int;
auto ptr = std::launder(&s);

s cannot refer to the newly created object.

I think he meant that the object was originally created by something like new const int(0).

Note that the storage is reusable ([basic.life] p10), but the rules of transparent replaceability (currently) don't specially treat such const objects ([basic.life] p8.3).

The difference between the original example and this example is, that there was ever an object in the storage referred by s while in the original example, there was no object in the storage.

jensmaurer commented 8 months ago

I'm not sure how this, rather clear, utterance gets mangled into an example not involving pointers and not involving const objects:

But that's true. You can dynamically allocate a block of memory and construct a const object into it. Take its address and store it somewhere. Then destroy the object and create a new one and repeat this many times.

A more applicable example would be this:

#include <new>
unsigned char * pmem = new unsigned char[sizeof(const int)];
const int * p1 = new (pmem) const int(0);
const int * p2 = new (pmem) const int(0);   // destroys object from previous line due to storage reuse
const int * p3 = new (pmem) const int(0);   // destroys object from previous line due to storage reuse
// You need to apply std::launder to p1 and p2, because const complete objects are not transparently replaceable.
xmh0511 commented 8 months ago

I'm not sure how this, rather clear, utterance gets mangled into an example not involving pointers and not involving const objects:

But that's true. You can dynamically allocate a block of memory and construct a const object into it. Take its address and store it somewhere. Then destroy the object and create a new one and repeat this many times.

A more applicable example would be this:

#include <new>
unsigned char * pmem = new unsigned char[sizeof(const int)];
const int * p1 = new (pmem) const int(0);
const int * p2 = new (pmem) const int(0);   // destroys object from previous line due to storage reuse
const int * p3 = new (pmem) const int(0);   // destroys object from previous line due to storage reuse
// You need to apply std::launder to p1 and p2, because const complete objects are not transparently replaceable.

I think this is the issue mentioned by @t3nsor

The problem is that the wording used in the standard doesn't always reflect the "consensus framework" (and there's a lot of work that needs to be done to fix it).

jensmaurer commented 8 months ago

The problem is that the wording used in the standard doesn't always reflect the "consensus framework" (and there's a lot of work that needs to be done to fix it).

For the particular example I gave, I don't think so. See these references:

Note that the storage is reusable ([basic.life] p10), but the rules of transparent replaceability (currently) don't specially treat such const objects ([basic.life] p8.3).

xmh0511 commented 8 months ago

The problem is that the wording used in the standard doesn't always reflect the "consensus framework" (and there's a lot of work that needs to be done to fix it).

For the particular example I gave, I don't think so. See these references:

Note that the storage is reusable ([basic.life] p10), but the rules of transparent replaceability (currently) don't specially treat such const objects ([basic.life] p8.3).

For an implicitly created array object that has its elements of non-implicit lifetime type, do these subobjects ever exist after the complete object is once created?

jensmaurer commented 8 months ago

I said "For the particular example I gave". My example did not involve an array object. Yet, you quoted my example and said "I think this is the issue mentioned...", referring to the "framework". We have a disconnect somewhere.

xmh0511 commented 8 months ago

I said "For the particular example I gave".

Oh, for your example, that's the case of what the normative rule says, the difference here is that there was an object before performing p2 and p3, such that [basic.life] p8 is suitable for p2 and p3.

a new object is created at the storage location which the original object occupied