Open xmh0511 opened 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.
Looks like a duplicate of #378.
I believe currently everything will be fine (modulo allocation failure) if we replace
#1
withauto 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 ofstd::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?
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 bythird_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.
I meant whether
std::launder
is required to obtain the pointerele
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:
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.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.(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.
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?
I meant whether
std::launder
is required to obtain the pointerele
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.
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.
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 declarationsT x
), it says that the object of typeT
is created. And that's all. But we always considered subobjects to exist afternew T
orT 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.
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?
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.
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
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.
B::b
is dead, which means, it does not have a lifetime
No idea how you manage to come to such conclusions
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).
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".
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.
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.
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.
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).
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.
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'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).
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).
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?
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.
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
Full name of submitter (unless configured in github; will be published with the issue): Jim X
Consider this example:
The malloc at
#2
can be considered to implicitly create an object of type array of 10A
but these subobjects are not because typeA
is not implicit-lifetime type, andptr
is considered to point to the first element of the array, then we create an object of typeA
at the storage of the element. However, is#3
UB or not? According to [basic.life] p8The
third_element_ptr
does not point to an object anyhow because there is no object created at the storage pointed bythird_element_ptr
at all.Suggested Resolution
Is
std::launder
necessary here to obtain the pointerele
to point to the created object? Or, should [basic.life] p8 also cover this case?