Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
geryogam opened this issue Sep 14, 2021 · 18 comments

Comments

@geryogam
Copy link
Contributor

geryogam commented Sep 14, 2021

[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?

@geryogam geryogam changed the title [basic.life] Does storage reuse update old pointers/references/names when the original object’s lifetime has not ended? [basic.life] Does storage reuse update pointers/references/names when the original object’s lifetime has not ended? Sep 14, 2021
@xmh0511
Copy link
Contributor

xmh0511 commented Sep 15, 2021

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
Copy link
Contributor Author

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
Copy link
Contributor

xmh0511 commented Sep 15, 2021

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
Copy link
Contributor Author

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.

  • The initial state is original object’s storage not reused nor released.
  • The next state is original object’s storage reused by a new object.
  • So the next state should be original object’s lifetime ended (because of storage reuse in the previous state).
  • The next states are the consequences.

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

@xmh0511
Copy link
Contributor

xmh0511 commented Sep 15, 2021

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
Copy link
Contributor Author

geryogam commented Sep 15, 2021

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
Copy link
Contributor Author

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

@xmh0511
Copy link
Contributor

xmh0511 commented Sep 18, 2021

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
Copy link
Member

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

@geryogam
Copy link
Contributor Author

geryogam commented Sep 19, 2021

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
Copy link
Contributor

xmh0511 commented Sep 23, 2021

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
Copy link
Contributor Author

geryogam commented Sep 23, 2021

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
Copy link
Contributor

xmh0511 commented Sep 23, 2021

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
Copy link
Contributor

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
Copy link
Contributor

xmh0511 commented Jan 8, 2022

@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
Copy link
Contributor

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
Copy link
Contributor

xmh0511 commented Jan 8, 2022

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
Copy link
Contributor

xmh0511 commented Sep 30, 2022

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"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants