Open geryogam opened 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.
Alright for the condition ‘no other storage reuse’ covering concurrency. But for the condition ‘original object’s lifetime has ended’, why is it necessary?
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.
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.
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.
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’?
@xmh0511 You seem to concur with me that the condition ‘whose lifetime has ended’ is unnecessary.
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.
Could I ask for a specific wording suggestion to be considered? (I don't think an outright defect has been identified here.)
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, […]
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?
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.
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).
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 union
s:
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.
@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
.
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.
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.
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"
I think CWG2863 will resolve this.
[basic.life/8] specifies (bold emphasis of the condition mine):
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?