c++multithreadinglanguage-lawyerlifetimehappens-before

Ending the lifetime of an object used in another thread


What can the Standard say about the code below?

#include <iostream>
#include <chrono>
#include <thread>

int main()
{
    static_assert(sizeof(int) == sizeof(float));
    using namespace std::chrono_literals;

    auto pi = new int{10};

    std::thread t([pi]() {
        for (int i = 0; i < 3; i++) {
            std::cout << *pi << '\n';
            std::this_thread::sleep_for(1s);
        }
    });

    std::this_thread::sleep_for(1s);
    auto pf = ::new (pi) float{};

    *pf = -1.0;

    t.join();
}

I'm especially curious how to (or, is it possible to) apply [basic.life]/7 and [basic.life]/8 saying

Similarly, before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released

and

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

respectively, given that [basic.life]/11 is saying

In this section, “before” and “after” refer to the “happens before” relation.

Does this mean that if the thread doesn't "see" the int object lifetime end, it can access it as if it was alive?

At first I thought that the program has a data race:

The execution of a program contains a data race if it contains two potentially concurrent conflicting actions, at least one of which is not atomic, and neither happens before the other, except for the special case for signal handlers described below. Any such data race results in undefined behavior.

However, there are no conflicting actions. By the definition of "conflict":

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

and the definition of "memory location":

A memory location is either an object of scalar type or a maximal sequence of adjacent bit-fields all having nonzero width.

reads through *pi don't conflict with store through *pf, because these lvalues denote different objects and thus different memory locations.

I feel the program has to have UB, but don't see where.


Solution

  • [basic.life]/4 The properties ascribed to objects and references throughout this International Standard apply for a given object or reference only during its lifetime.

    The term "during" is not formally defined, but it would be reasonable to define it as "after the lifetime starts and before the lifetime ends".

    Now, the opposite of happens-after is not happens-before, it's "happens-before or is unsynchronized-with". Thus, an operation that happens-before or is unsynchronized-with the start of the object's lifetime, or that happens-after or is unsynchronized-with the end of the object's lifetime, is not performed "during its lifetime". To the extent such an operation relies on "properties ascribed to" this object, it exhibits undefined behavior.

    On these grounds, I believe your example exhibits undefined behavior, as access to the object *pi is unsynchronized-with the end of its lifetime.