7

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.

Tavian Barnes
  • 12,477
  • 4
  • 45
  • 118

2 Answers2

3

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.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • 1
    Right, I'm not particularly worried about the code in the destructor, it's really the call to bar() from the other thread, which may execute concurrently with the destructor, that worries me. – Tavian Barnes Sep 12 '17 at 16:32
  • @TavianBarnes What other thread? You have to call `bar_async()` to start the member thread. You can't call that function at the same time as the object is being destructed in your example. – NathanOliver Sep 12 '17 at 16:35
  • @TavianBarnes Yes, the member thread runs the function `bar` but you started that before the destructor was called. That means when you get to the destructor the thread will be joinable and you will wait for the function to end before you end the destructor so `bar` still refers to a valid object and there is no UB. – NathanOliver Sep 12 '17 at 16:41
  • Quoting from [here](http://en.cppreference.com/w/cpp/language/destructor#Trivial_destructor) *"...after the body of the destructor is executed, the compiler calls the destructors for all non-static non-variant members of the class..."* Thus `bar` is still alive until the thread joins. – Ripi2 Sep 12 '17 at 16:43
0

My intuition is no. This is because thread::join can throw an exception and you don't want exceptions escaping your destructor. It may be okay if you wrap it in a try catch and handle the exception properly though.

Marcus Karpoff
  • 451
  • 5
  • 16
  • 3
    Note that the causes of these exceptions are basically programming errors - trying to join yourself, or trying to join an non-joinable thread. – MSalters Sep 12 '17 at 16:31
  • An exception within a thread terminates the programm immediately, if the exception is not catched and handled in the thread that has it thrown. - A destructor joining that thread is not affected at all. – CAF Sep 12 '17 at 18:12