Assuming X
and Y
are suitable types for such usage, is it UB to use std::start_lifetime_as<X>
on an area of memory in one thread as one type and use std::start_lifetime_as<Y>
on the exact same memory in another thread? Does the standard say anything about this? If it doesn't, what is the correct interpretation?
Object lifetime is actually one of the more underspecified parts of the standard, especially when it comes to concurrency (and in some places the wording is outright defective IMO), but I think this specific question is answerable with what's there.
First, let's get data races out of the way.
The execution of a program contains a data race if it contains two potentially concurrent conflicting actions [...]
Two expression evaluations conflict if one of them modifies a memory location and the other one reads or modifies the same memory location.
A memory location is either an object of scalar type that is not a bit-field or a maximal sequence of adjacent bit-fields all having nonzero width.
Two unrelated objects are definitely not the same 'memory location', so [intro.races]/21 doesn't apply.
However, [intro.object]/9 says:
Two objects with overlapping lifetimes that are not bit-fields may have the same address if one is nested within the other, or if at least one is a subobject of zero size and they are of different types; otherwise, they have distinct addresses and occupy disjoint bytes of storage.
This means that out of any two (unrelated) objects with overlapping storage, at most one can be within lifetime at any given point. [basic.life]/1.5 ensures this:
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.
Accessing (reading or writing) an object outside its lifetime is not allowed ([basic.life]/4), and we've just established that X
and Y
can't both be within lifetime at the same time. So, if both threads proceed to access the created objects, the behavior is undefined: at least one will be accessing an object whose lifetime has ended.
There is no data race from such calls, since none of them access any memory locations, but since (without synchronization) neither thread can know that the other has not ended the lifetime of its desired object by reusing its storage for an object of the other type, the objects created cannot be used. (There are not “even odds” that one thread can use them because it “went last”: there is an execution where it didn’t, so relying on that would have undefined behavior.)
Object lifetime is actually one of the more underspecified parts of the standard, especially when it comes to concurrency (and in some places the wording is outright defective IMO), but I think this specific question is answerable with what's there.
First, let's get data races out of the way.
The execution of a program contains a data race if it contains two potentially concurrent conflicting actions [...]
Two expression evaluations conflict if one of them modifies a memory location and the other one reads or modifies the same memory location.
A memory location is either an object of scalar type that is not a bit-field or a maximal sequence of adjacent bit-fields all having nonzero width.
Two unrelated objects are definitely not the same 'memory location', so [intro.races]/21 doesn't apply.
However, [intro.object]/9 says:
Two objects with overlapping lifetimes that are not bit-fields may have the same address if one is nested within the other, or if at least one is a subobject of zero size and they are of different types; otherwise, they have distinct addresses and occupy disjoint bytes of storage.
This means that out of any two (unrelated) objects with overlapping storage, at most one can be within lifetime at any given point. [basic.life]/1.5 ensures this:
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.
Accessing (reading or writing) an object outside its lifetime is not allowed ([basic.life]/4), and we've just established that X
and Y
can't both be within lifetime at the same time. So, if both threads proceed to access the created objects, the behavior is undefined: at least one will be accessing an object whose lifetime has ended.
© 2022 - 2024 — McMap. All rights reserved.
start_lifetime_as
is a data race. – Paramaribonew
on the same memory a data race? Because I can't find the answer to that either. [new.delete.dataraces] suggests that it's not a data race somehow, but that cannot possibly make sense, as placementnew
shouldn't do anything. – Urinabit_cast
on the data in that memory, except that no accesses are performed. New expressions do not promise to preserve the bytes in the storage. – Urinanew(memory) T;
does not call a constructor ofT
if it is trivially default constructible. The created object is left uninitialized. Andstart_lifetime_as
never calls a constructor. It initializes the object with the data in the storage. – Urinastart_lifetime_as
, while they are not with placement-new. When I said "uninitialized", that doesn't mean unchanged. When an object is not initialized, its value is unspecified.start_lifetime_as
specifies the value of the object. – Urina