Search code examples
c++multithreadingc++11thread-safetyobject-lifetime

Is it safe to wait for asynchronous work by joining in the destructor?


Suppose I have a class that may run some code asynchronously, and that asynchronous code uses that class instance to do things like call member functions, read data members, etc. Obviously the class instance must outlive the background thread in order for those accesses to be safe. It is sufficient to ensure this by joining the background thread in the destructor? For example:

#include <iostream>
#include <thread>

class foo final
{
public:
    foo() = default;

    void bar() {
        std::cout << "Hopefully there's nothing wrong with using " << this << "\n";
    }

    void bar_async() {
        if (!m_thread.joinable()) {
            m_thread = std::thread{&foo::bar, this};
        }
    }

    ~foo() {
        if (m_thread.joinable()) {
            std::cout << "Waiting for " << m_thread.get_id() << "\n";
            m_thread.join();
        }
    }

private:
    std::thread m_thread;
};

int main() {
    foo f;
    f.bar_async();
}

Specifically, I'm worried about object lifetime rules:

For any object of class types whose destructor is not trivial, lifetime ends when the execution of the destructor begins.

... after the lifetime of an object has ended and before the storage which the object occupied is reused or released, the following uses of the glvalue expression that identifies that object are undefined: ...

  • Access to a non-static data member or a call to a non-static member function.

But to me, a strict reading of the above would also imply that calling this->bar() from inside ~foo() directly is undefined, which is "obviously" not the case.


Solution

  • cppreference is right but it is talking about accessing the members from the object, not from inside the destructor. If we look at [class.cdtor]/1 we see that

    For an object with a non-trivial constructor, referring to any non-static member or base class of the object before the constructor begins execution results in undefined behavior. For an object with a non-trivial destructor, referring to any non-static member or base class of the object after the destructor finishes execution results in undefined behavior.

    emphasis mine

    So, as long as we are in the destructor we can still work with the member objects as those are not destroyed until the scope of the destructor ends.

    So, calling join on the thread is fine here. If you think about it if it wasn't then things like lock guards would be useless if accessing the mutex they refer to was undefined behavior.