c++language-lawyerundefined-behaviorobject-lifetimec++23

std::start_lifetime_as and UB in C++23 multithreaded application


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?


Solution

  • 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.

    [intro.races]/21:

    The execution of a program contains a data race if it contains two potentially concurrent conflicting actions [...]

    [intro.races]/2:

    Two expression evaluations conflict if one of them modifies a memory location and the other one reads or modifies the same memory location.

    [intro.memory]/3:

    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.