cplusplus / draft

C++ standards drafts
http://www.open-std.org/jtc1/sc22/wg21/
5.69k stars 749 forks source link

[basic.life] Does storage reuse update pointers/references/names when the original object’s lifetime has not ended? #4906

Open geryogam opened 3 years ago

geryogam commented 3 years ago

[basic.life/8] specifies (bold emphasis of the condition mine):

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.

Basically the condition is this: if a new object reuses the storage occupied by an original object whose lifetime has ended, then…

But why isn’t it just this (i.e. removing the condition ‘whose lifetime has ended’): if a new object reuses the storage occupied by an original object, then…

In other words, does the consequence on pointers/references/names referring to the original object still apply with that less strict condition?

xmh0511 commented 3 years ago

Note the "before" and "after"

In this subclause, “before” and “after” refer to the “happens before” relation ([intro.multithread]).

It means the subclause is intended to cover concurrent cases. We should clearly phrase what the state of the storage is at that point time. In short, only if there are no other objects reuse the storage at that time and the lifetime of the original object occupied that storage has ended, the newly created object at that time can be granted to have these properties.

geryogam commented 3 years ago

Alright for the condition ‘no other storage reuse’ covering concurrency. But for the condition ‘original object’s lifetime has ended’, why is it necessary?

xmh0511 commented 3 years ago

Because we want to stress the concept automatically refers to the new object. A pointer may be obtained during the lifetime of the original object, the pointer hence points to that object through the lifetime of that object. Once the lifetime of that object has ended, the pointer is still considered to point to that object that has been expired. Hence, in this situation, a new object created at the storage that satisfies certain conditions can make the pointer/reference be refreshed to refer to the new object.

To end the lifetime of an object, it is either the object is destroyed or its destructor is called, or the storage the object occupies is reused. Since it is associated with concurrent, and we should clearly expound the state as the above said, the original object’s lifetime has ended is necessary.

geryogam commented 3 years ago

To end the lifetime of an object, it is either the object is destroyed or its destructor is called, or the storage the object occupies is reused.

… or released.

Since it is associated with concurrent, and we should clearly expound the state as the above said, the original object’s lifetime has ended is necessary.

So I still don’t get why original object’s lifetime ended is part of the initial state in the standard.

xmh0511 commented 3 years ago

Assume the initial wording was "if a new object reuses the storage occupied by an original object", if there are multi threads, in each thread there is a new object that will create at that storage location. Which is the new object and the original object we are saying? In other words, is there more simple way to phrase the between-situation? or stress that this rule only applies to a particular object at some time.

geryogam commented 3 years ago

if there are multi threads, in each thread there is a new object that will create at that storage location.

How is it possible since we assumed ‘before the storage which the object occupied is reused’?

geryogam commented 3 years ago

@xmh0511 You seem to concur with me that the condition ‘whose lifetime has ended’ is unnecessary.

xmh0511 commented 3 years ago

I still retain my opinions that differ from yours. I don't know what way will you rephrase that rule with your wording. Maybe, you should leave your proposal here, and further analysis could be done according to that proposal.

jensmaurer commented 3 years ago

Could I ask for a specific wording suggestion to be considered? (I don't think an outright defect has been identified here.)

geryogam commented 3 years ago

I would just remove the end of lifetime requirement which seems to me unnecessary, that is I would transform

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, […]

into

If, before the storage which an object occupied is reused or released, a new object is created at the storage location which the original object occupied, […]

xmh0511 commented 3 years ago

Consider a situation, if a value computation of the original object by a name/pointer/reference and the application of [basic.life/8] you have modified have an exact overlap(i.e happen at the same time, especially in a multi-thread scene), which is the object the value computation performs on?

geryogam commented 3 years ago

Here is the timeline with the requirement that the original object’s lifetime has ended (the current standard):

|------------------------|---|-------------------------|-------------------|
 original object lifetime GAP new object initialization new object lifetime

Here is the timeline without the requirement that the original object’s lifetime has ended (my proposal):

|------------------------|-------------------------|-------------------|
 original object lifetime new object initialization new object lifetime

which is the object the value computation performs on?

I think it is independent of the presence or absence of the GAP above. If you retrieve a value during the original object lifetime, then you get its value. If you retrieve a value during the new object lifetime, then you get its value. If you retrieve a value during the GAP (if any, cf. my proposal) or during the new object initialization, then you get an indeterminate value.

xmh0511 commented 3 years ago

If we have a "GAP" situation, any value computation or side effect that performs during this situation has explicitly undefined behavior. "original object’s lifetime has ended" has this effect that we cannot use the name/reference/pointer to manipulate the object until the lifetime of a new object has started, which could arguably be called a bound. If we do not give that "GAP", as I said above, any value computation or side effect by using the "name/pointer/reference" should have been well-formed but the overlap action(create a new object) would make that context vague(break the well-formed operation).

languagelawyer commented 2 years ago

I don't think an outright defect has been identified here

@jensmaurer The issue is that the paragraphs says «If, after the lifetime of an object has ended», but we want it to apply in case when we reuse the storage of an object whose lifetime can't be ended¹, which happens to unions:

union U
{
  int i;
  float f;
} u; // don't remember if this makes U::i active, U::f is not active for sure

u.f = 0; // to make this defined behavior
// [class.union] creates a new object of float type
// however, since u.f denoted an object which has never been alive
// the creation of the new object doesn't end the old one's lifetime
// and the paragraph does not "rebind" U::f to the new object

1 This relies on the following observation: [intro.object]: An object occupies a region of storage in its period of construction, throughout its lifetime, and in its period of destruction. [basic.life]: The lifetime of an object o of type T ends when: … the storage which the object occupies is released, or is reused by an object that is not nested within o

Note the present tense. Since a dead object (which is also not during its periods) does not occupy the storage, its lifetime can not ended by its former/potential storage reuse.

xmh0511 commented 2 years ago

@languagelawyer

union U
{
  int i;
  float f;
} u; // don't remember if this makes U::i active, U::f is not active for sure

It seems that neither of the members is active, did you mean

union U
{
  int i;
  float f;
} u{};  // as per [dcl.init.aggr#5.5] and [dcl.init.list#3.11], the first variant member is initialized and active. 

It is indeed an issue that, since u.f has never been constructed, hence it has never occupied storage. I think u.f is not able to be called a dead object since it has/had never existed at all. Although, for u.f = 0;, [class.union#general-6] regulates that an object of the type of X is implicitly created in the nominated storage, what's the nominated storage? Moreover, the first bullet of [basic.life#8] requires that the object denoted byu.f at least has/had occupied a storage. So, even we state "an object (o2) of the type of X is implicitly created in the nominated storage", but o2 is not transparently replaceable with the object(o1) denoted by u.f.

languagelawyer commented 2 years ago

It seems that neither of the members is active, did you mean

I needed U::f not to be active for sure. What happens to U::i doesn't matter.

I think u.f is not able to be called a dead object since it has/had never existed at all

If this conclusion comes from that the Standard doesn't say how/when the corresponding subobject appear, then this is not unique for unions. For U declared with struct instead of union, nothing says that subobjects corresponding to U::i and U::f emerge when their containing object is created, it is just assumed that they exist.

So I think the implied model is that when an object, whether of union or non-union class type, is created, it has subobjects corresponding to each NSDM, even though, in the union case, at most one of them is initialized and brought to life.

xmh0511 commented 2 years ago

If the concern wasn't arisen from "whether storage occupied by a subobject corresponding to a member even though it was not be constructed", I didn't see the issue in [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, ..., An object o1 is transparently replaceable by an object o2 if:

  • the storage that o2 occupies exactly overlays the storage that o1 occupied, and
  • [...]

In this provision, it uses "occupied" for object o1, which means, the storage can be occupied by o1 in the past, as long as o1 ever has occupied the storage. Even if there is an intervening object that occupies the storage after o1, the storage was also occupied by o1 in the past.

Back to this example

union U
{
  int i;
  float f;
} u{};

the object associated with U::f occupied the storage, even if the current object that occupies the storage is associated with U::i. I think "the lifetime of U::f is never alive" can be subsumed to "after the lifetime of an object has ended". So, the condition of [basic.life] p8 is true in this case. [class.union#general-6] says " an object of the type of X is implicitly created in the nominated storage", which is the object o2 in [basic.life] p8. When o1 is the object associated with U::f and o2 is the object [class.union#general-6] implicitly created, they satisfy all bullets of [basic.life] p8.

xmh0511 commented 2 years ago

Also, the sentence seems to have a logical paradox.

[basic.life] p1 says

The lifetime of an object o of type T ends when:

  • if T is a non-class type, the object is destroyed, or
  • if T is a class type, the destructor call starts, or
  • the storage which the object occupies is released, or is reused by an object that is not nested within o ([intro.object]).

Either requirement that is satisfied will end the lifetime of the object. The third bullet causes the paradox here. [basic.life] p8 says

If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released

Note that the storage reuse can end the object's lifetime. In the formal example, there is no problem

this->~C();      // explicitly invoke the destructor ends the lifetime 
new (this) C(other);  // the evaluation of the creating new object happens after the lifetime is ended

However, consider this case: if we do not explicitly call the destructor, instead, by directly creating the new object, which will reuse the storage

new (this) C(other);  // reuse storage ends the lifetime

The storage reuse results in the lifetime being ended. So, which one is counting to happen before the other? In common sense, the reuse action should occur first such that the lifetime of the original object will be ended due to the reuse. That is to say [basic.life] p8 seems not to be suitable for the second case?


In addition, the evaluation of new (this) C(other) produces two actions: creating the new object and reusing the storage. which one happens first? After all, we require that

"a new object is created" should happen before "the storage which the object occupied is reused or released"

frederick-vs-ja commented 2 months ago

I think CWG2863 will resolve this.