Clarification and reasons for object lifetime constraints change in C++20 - c++

As of C++20, there's a significant change for the constraints of object lifetime from basic.life#8.3 to
n4861/basic.life#8.3. The concrete change I want to focus on here is (C++20 draft)
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. An object o1 is transparently replaceable by an object o2 if
the storage that o2 occupies exactly overlays the storage that o1 occupied, and
o1 and o2 are of the same type (ignoring the top-level cv-qualifiers), and
o1 is not a complete const object, and
neither o1 nor o2 is a potentially-overlapping subobject, and
either o1 and o2 are both complete objects, or o1 and o2 are direct subobjects of objects p1 and p2, respectively, and p1 is
transparently replaceable by p2.
vs. the pre-C++20 one (not the only change, see the drafts for details)
the type of the original object is not const-qualified, and, if a
class type, does not contain any non-static data member whose type is
const-qualified or a reference type...
Question 1:
Maybe too obvious, just to be sure: With complete const object, the standard refers to complete objects that are const, right? (So it has nothing to do with partial/full const objects that was part of the previous quality of wording within this section?)
Question 2:
Can anyone explain the reasons behind these changes (aliasing?)?
Question 3:
Is my assumption valid, that this one is a heavy relaxation of the former rules, i.e. guaranteeing now a lot more object/memory reusage scenarios not to be UB for instance? In doubt, shouldn't they affect the way the optimizers are allowed to operate in a heavy way too (shift of efficiency fields)?

Related

Does std::optional<>::emplace() invalidate references to the inner value?

Consider the following fragment (assume that T is trivially constructible and trivially destructible):
std::optional<T> opt;
opt.emplace();
T& ref = opt.value();
opt.emplace();
// is ref guaranteed to be valid here?
From the definition of std::optional we know that the contained instance is guaranteed to be allocated inside the std::optional container, hence we know that the reference ref will always be referring to the same memory location. Are there circumstances where said reference will not retain validity after the pointed-to object is destroyed and then constructed again?
C++20 has the following rule, [basic.life]/8:
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. An object o1 is transparently replaceable by an object o2 if:
the storage that o2 occupies exactly overlays the storage that o1 occupied, and
o1 and o2 are of the same type (ignoring the top-level cv-qualifiers), and
o1 is not a complete const object, and
neither o1 nor o2 is a potentially-overlapping subobject (6.7.2), and
either o1 and o2 are both complete objects, or o1 and o2 are direct subobjects of objects p1 and p2 ,
respectively, and p1 is transparently replaceable by p2.
This suggests that as long as T is not const-qualified, destroying the T inside an std::optional<T> and then recreating it should result in a reference to the old object automatically referring to the new object. As pointed out in the comments section, this is a change from the old behaviour, abolishing a requirement that T must not contain a non-static data member of const-qualified or reference type. (Edit: I previously asserted that the change was made retroactively, as I confused it with a different change in C++20. I am not sure whether the resolution to RU 007 and US 042 as indicated in N4858 were made retroactive, but I suspect the answer is yes, because the change was needed to fix code involving standard library templates that was probably not intended to be broken from C++11 through C++17.)
However, we are making the assumption that the new T object is being created "before the storage which the [old] object occupied is reused or released". If I were writing an "adversarial" implementation of the standard library, I could set it up so that the emplace call reuses the underlying storage prior to creating the new T object. This would prevent the old T object from being transparently replaced by the new one.
How might an implementation "reuse" the storage? Typically, the underlying storage might be declared like this:
union {
char no_object;
T object;
};
When the default constructor of optional is called, no_object is initialized (the value does not matter)1. An emplace() call checks whether there is a T object or not (by checking a flag that is not shown here). If a T object is present, then object.~T() is called. Finally, something similar to construct_at(addressof(object)) is called in order to construct the new T object.
Not that any implementation would ever do this, but you could imagine an implementation that, in between the calls to object.~T() and construct_at(addressof(object)), re-initializes the no_object member. This would be a "reuse" of the storage that was previously occupied by object. This would imply that the requirements of [basic.life]/8 are not met.
Of course, the practical answer to your question is that (1) there is no reason for an implementation to do something like this, and (2) even if an implementation did it, the developers would ensure that your code still behaves as if the T object was transparently replaced. Your code is reasonable under the assumption that the standard library implementation is reasonable, and compiler developers do not like to break code with that property, since doing so would needlessly aggravate their users.
But if a compiler developer were inclined to break your code (based on the argument that the more undefined behaviour there is, the more the compiler can optimize) then they could break your code even without changing the <optional> header file. The user is required to treat the standard library like a "black box" that only guarantees what the standard explicitly guarantees. So under a pedantic reading of the standard, it's unspecified whether or not attempting to access ref after the second emplace call has undefined behaviour. If it's unspecified whether it's UB, then the compiler is allowed to start treating it as UB whenever it wants.
1 The reason for this is historical; C++17 requires that a constexpr constructor initialize exactly one variant member of a union. This rule was abolished in C++20, so a C++20 implementation could omit the no_object member.

Which version of C++ standard allows reuse of storage previously occupied by an object of a class that has const or reference members?

This answer cites some unknown revision of C++ standard draft:
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 storage for the new object exactly overlays the storage location which the original object occupied, and
the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and
the type of the original object is not const-qualified, and, if a class type, does not contain any non-static data member whose type is const-qualified or a reference type, and
neither the original object nor the new object is a potentially-overlapping subobject ([intro.object]).
This means that the following code is invalid if class A has const or reference members:
A a;
a.~A();
new (&a) A;
The current revision of [basic.life]p8 doesn't have this requirement:
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. An object o1 is transparently replaceable by an object o2 if:
the storage that o2 occupies exactly overlays the storage that o1 occupied, and
o1 and o2 are of the same type (ignoring the top-level cv-qualifiers), and
o1 is not a complete const object, and
neither o1 nor o2 is a potentially-overlapping subobject ([intro.object]), and
either o1 and o2 are both complete objects, or o1 and o2 are direct subobjects of objects p1 and p2, respectively, and p1 is transparently replaceable by p2.
This make the code above valid.
But both citations are from the draft. So I don't know starting from witch version of the standard I can use the code above for the class objects that have const or reference members. The answer date is May 7, 2018. So I guess it can be only C++20?
In terms of the "Major" Standard releases, the clause about the const qualification (which you have emphasised in the excerpt you cite in your question) was present in the final draft for the C++17 Standard (N4659) but not present in that for the C++20 Standard (N4861).
So, from that it would appear that conformance to C++20 (or later) is required to reuse storage previously occupied by a const- or reference-containing class object (in this context).

What is the relationship between the end of object's lifetime and when it ceases to exist?

In the following short example, what can be said about the object the pointer f points to or used to point to just before returning from main?
#include <vector>
struct foo {
std::vector<int> m;
};
int main()
{
auto f = new foo;
f->~foo();
}
I believe that there is no longer an object foo where f used to point. I've received a lot of comments that this may not be correct and that instead there could be an object foo in a destroyed, dead or otherwise invalid state.
What does the language standard have to say about the existence of an objects that is explicitly destroyed but whose storage is still valid?
In other words, can it reasonably be said that there is still an object at f that is outside of its lifetime? Is there such a thing as an object that is not in its lifetime, not begin constructed and not being destructed?
Edit :
It is clear that an object can exist when it isn't in its lifetime. During construction and destruction there is an object and its lifetime has not yet begun or as already ended. From https://timsong-cpp.github.io/cppwp/intro.object#1 :
[...] An object occupies a region of storage in its period of construction ([class.cdtor]), throughout its lifetime, and in its period of destruction ([class.cdtor]). [...]
But after f->~foo(); the object that was pointed to by f (lets call it o) is not being constructed, it is not in its lifetime and it is not being destructed. My reading of this section is that o cannot occupy the storage anymore because it isn't in any of the enumerated situations. It seems like this implies that there is no o anymore and that there can't be a pointer to o anymore. By contradiction, if you had a pointer to o then that pointer would point to storage which o can't occupy.
Edit 2 :
If there isn't an object anymore, then what kind of value does foo have? It seems like the only sensible possible value it can have is a pointer to an object, which would contradict the statement. See this question.
In C++, objects essentially are eternal. There's nothing in the language that makes an object disappear. An object that is outside of its lifetime is still an object, it still occupies storage, and the standard has specific things that you can do with a pointer/reference to an object which is outside of its lifetime.
An object only truly goes away when it is impossible to have a valid pointer/reference to it. This happens when the storage occupied by that object ends its storage duration. A pointer to storage that is past its duration is an invalid pointer, even if the address itself later becomes valid again.
So by calling the destructor instead of using delete f (which would also deallocate the storage), f remains pointing to an object of type foo, but that object is outside of its lifetime.
The justification for my above statements basically boils down to the standard having none of the provisions that it would need in order to support the concept of objects being uncreated.
Where is object uncreation?
The standard provides clear, unequivocal statements about when an object comes to exist within a piece of storage. [intro.object]/1 outlines the exact mechanisms that provoke the creation of an object.
The standard provides clear, unequivocal statements about when an object's lifetime begins and ends. [basic.life] in its entirely outlines these things, but [basic.life]/1 in particular explains when an object's lifetime begins and ends.
The standard does not provide any statement (clear or otherwise) about when an object no longer exists. The standard says when objects are created, when their lifetimes begin, and when they end. But never does it say when they stop existing within a piece of storage.
There has also been discussion about statements of the form:
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.
Emphasis added.
The use of the past-tense suggests that the object is no longer located in that storage. But when did the object stop being located there? There is no clear statement about what exactly caused that to happen. And without that, the use of past-tense here just doesn't matter.
If you can't point to a statement about when it stopped being there, then the absolute most you can say is that there are a couple of places in the standard with wording that could be cleaned up. It doesn't undo the clear fact that the standard does not say when objects stop existing.
Pointer validity
But it does say when objects are no longer accessible.
In order for an object to cease existing, the standard would have to account for pointers which point to those objects when they no longer exist. After all, if a pointer is pointing to an object, then that object must still exist, right?
[basic.compound]/3 outlines the states that a pointer can have. Pointers can be in one of four states:
a pointer to an object or function (the pointer is said to point to the object or function), or
a pointer past the end of an object ([expr.add]), or
the null pointer value ([conv.ptr]) for that type, or
an invalid pointer value.
There is no allowance given for a pointer which points to no object. There is an allowance for an "invalid pointer value", but pointers only become invalid when the storage duration for the storage they point into ends:
When the end of the duration of a region of storage is reached, the values of all pointers representing the address of any part of that region of storage become invalid pointer values.
Note that this statement means that all pointers to such objects cease being in the "pointer to object" state and enter the "invalid pointer" state. Thus, objects within such storage (both within and outside of their lifetimes) stop being accessible.
This is exactly the sort of statement that would need to exist for the standard to support the concept of objects no longer existing.
But no such statement exists.
[basic.life] does have several statements that address limited ways that pointers to objects outside of their lifetime can be used. But note the specific wording it uses:
For an object under construction or destruction, see [class.cdtor]. Otherwise, such a pointer refers to allocated storage ([basic.stc.dynamic.deallocation]), and using the pointer as if the pointer were of type void*, is well-defined.
It never says that the pointer "points to" allocated storage. It never undoes [basic.compound]/3's declaration about the kinds of pointers. The pointer is still a pointer to an object; it's just that the pointer "refers to allocated storage". And that the pointer can be used as a void*.
That is, there's no such thing as a "pointer to allocated storage". There is a "pointer to an object outside of its lifetime, whose pointer value can be used to refers to allocated storage". But is still a "pointer to an object".
Lifetime is not existence
Objects must exist in order to have a lifetime. The standard makes that clear. However, the standard does not at any point link the existence of an object to its lifetime.
Indeed, the object model would be a lot less complicated if ending the lifetime of an object meant that the object didn't exist. Most of [basic.life] is about carving out specific ways you can use the name of an object or a pointer/reference to it outside of the lifetime of that object. We wouldn't need that sort of stuff if the object itself didn't exist.
Stated in discussion about this matter was this:
I believe mentions of out-of-lifetime objects are there to account for objects that are being constructed and objects that are being destructed.
If that were true, what is [basic.life]/8 talking about with this statement:
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
If pointers to the original object become pointers to allocated memory when the object's lifetime ends, why does this statement talk about pointers to the original object? Pointers can't point to objects that don't exist because they don't exist.
This passage can only make sense if those objects continue to exist outside of their lifetimes. And no, it's not just about within the constructor/destructor; the example in the section makes that abundantly clear:
struct C {
int i;
void f();
const C& operator=( const C& );
};
const C& C::operator=( const C& other) {
if ( this != &other ) {
this->~C(); // lifetime of *this ends
new (this) C(other); // new object of type C created
f(); // well-defined
}
return *this;
}
C c1;
C c2;
c1 = c2; // well-defined
c1.f(); // well-defined; c1 refers to a new object of type C
While operator= does call the destructor, that destructor finishes before the this pointer is used. Thus, the special provisions of of [class.cdtor] does not apply to this at the moment the new object is created. So the new object is created outside of the destructor call to the old one.
So it's very clear that the "outside its lifetime" rules for objects are meant to always work. It's not just a provision for constructors/destructors (if it was, it would explicitly call that out). This means that names/pointers/references must still name/point-to/reference objects outside of their lifetime until the creation of the new object.
And for that to happen, the object they name/point-to/reference must still exist.

Is it safe to call placement new on `this` for trivial object?

I know that this question was asked several times already but I couldn't find an answer for this particular case.
Let's say I have a trivial class that doesn't own any resources and has empty destructor and default constructor. It has a handful of member variables with in-class initialization; not one of them is const.
I want to re-initialize and object of such class it without writing deInit method by hand. Is it safe to do it like this?
void A::deInit()
{
new (this)A{};
}
I can't see any problem with it - object is always in valid state, this still points to the same address; but it's C++ so I want to be sure.
Similarly to the legality of delete this, placement new to this is also allowed as far as I know. Also, regarding whether this, or other pre-existing pointers / references can be used afterwards, there are a few restrictions:
[basic.life]
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 storage for the new object exactly overlays the storage location which the original object occupied, and
the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and
the type of the original object is not const-qualified, and, if a class type, does not contain any non-static data member whose type is
const-qualified or a reference type, and
neither the original object nor the new object is a potentially-overlapping subobject ([intro.object]).
The first two are satisfied in this example, but the last two will need to be taken into consideration.
Regarding the third point, given that the function is non-const-qualified, it should be fairly safe to assume that the original object is non-const. The fault is on the caller side if the constness has been cast away. Regarding const / reference member, I think that can be checked by asserting that this is assignable:
static_assert(std::is_trivial_v<A> && std::is_copy_assignable_v<A>);
Of course, since assignability is a requirement, you could instead simply use *this = {}; which I would expect to produce the same program. A perhaps more interesting use case might be to reuse memory of *this for an object of another type (which would fail the requirements for using this, at least without reinterpreting + laundering).
Similar to delete this, placement new to this could hardly be described as "safe".
The rules that cover this are in [basic.life]/5
A program may end the lifetime of any object by reusing the storage which the object occupies or by explicitly calling the destructor for an object of a class type. For an object of a class type, the program is not required to call the destructor explicitly before the storage which the object occupies is reused or released; however, if there is no explicit call to the destructor or if a delete-expression is not used to release the storage, the destructor is not implicitly called and any program that depends on the side effects produced by the destructor has undefined behavior.
and [basic.life]/8
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 storage for the new object exactly overlays the storage location which the original object occupied, and
the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and
the type of the original object is not const-qualified, and, if a class type, does not contain any non-static data member whose type is const-qualified or a reference type, and
neither the original object nor the new object is a potentially-overlapping subobject ([intro.object]).
Since your object is trivial you don't have to worry about [basic.life]/5 and as long as you satisfy the bullet points from [basic.life]/8, then it is safe.

Is replacing `this` with a different type allowed?

In the comments and answers to this question:
Virtual function compiler optimization c++
it is argued that a virtual function call in a loop cannot be devirtualized, because the virtual function might replace this by another object using placement new, e.g.:
void A::foo() { // virtual
static_assert(sizeof(A) == sizeof(Derived));
new(this) Derived;
}
The example is from a LLVM blog article about devirtualization
Now my question is: is that allowed by the standard?
I could find this on cppreference about storage reuse: (emphasis mine)
A program is not required to call the destructor of an object to end its lifetime if the object is trivially-destructible or if the program does not rely on the side effects of the destructor. However, if a program ends the lifetime of an non-trivial object, it must ensure that a new object of the same type is constructed in-place (e.g. via placement new) before the destructor may be called implicitly
If the new object must have the same type, it must have the same virtual functions. So it is not possible to have a different virtual function, and thus, devirtualization is acceptable.
Or do I misunderstand something?
The quote you provided says:
If a program ends the lifetime of an non-trivial object, it must ensure that a new object of the same type is constructed in-place (e.g. via placement new) before the destructor may be called implicitly
The intent of this statement relates to something a bit different to what you are doing. The statement is meant to say that when you destroy an object without destroying its name, something still refers to that storage with the original type, o you need to construct a new object there so that when the implicit destruction occurs, there is a valid object to destroy. This is relevant for example if you have an automatic ("stack") variable, and you call its destructor--you need to construct a new instance there before the destructor is called when the variable goes out of scope.
The statement as a whole, and its "of the same type" clause in particular, has no bearing on the topic you're discussing, which is whether you are allowed to construct a different polymorphic type having the same storage requirements in place of an old one. I don't know of any reason why you shouldn't be allowed to do that.
Now, that being said, the question you linked to is doing something different: it is calling a function using implicit this in a loop, and the question is whether the compiler could assume that the vptr for this will not change in that loop. I believe the compiler could (and clang -fstrict-vtable-pointers does) assume this, because this is only valid if the type is the same after the placement new.
So while the quotes from the standard you have provided are not relevant to this issue, the end result is that it does seem possible for an optimizer to devirtualize function calls made in a loop under the assumption that the type of *this (or its vptr) cannot change. The type of an object stored at an address (and its vptr) can change, but if it does, the old this is no longer valid.
It appears that you intend to use the new object using handles (pointers, references, or the original variable name) that existed prior to its recreation. That's allowed only if the instance type is not changed, plus some other conditions excluding const objects and sub-objects:
From [basic.life]:
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 storage for the new object exactly overlays the storage location which the original object occupied,
and
the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and
the type of the original object is not const-qualified, and, if a class type, does not contain any non-static data member whose type is const-qualified or a reference type, and
the original object was a most derived object of type T and the new object is a most derived object of type T (that is, they are not base class subobjects).
Your quote from the Standard is merely a consequence of this one.
Your proposed "devirtualization counter-example" does not meet these requirements, therefore all attempts to access the object after it is replaced will cause undefined behavior.
The blog post even pointed this out, in the very next sentence after the example code you looked at.