When we start a thread, there are two options. Either we will wait for the thread to finish or we will not wait for it. A mechanism for waiting is a .join() member function. This is what we did in the first article. The .detach() member function is the other option: not waiting for the thread to finish.

Therefore, we must choose between these two options. If we don’t detach or join the thread, the program will crash! If you don’t believe me, comment a .join() call in helloWorld.cpp, compile and run the program. You should get an error message:

...
terminate called without an active exception
Aborted (core dumped)
...

Two options

Thus, we must call either join or detach. The second option is not problematic. The .detach() is invoked right after the construction of a thread.

The first approach, however, has some problems. Invoking the .join() right after constructor doesn’t make sense, because there is no need for the new thread in this case. Therefore, there is some code between the construction of the thread and the .join() call.

int main()
{
    std::thread t(function);

    ...
    some code
    ...
    
    t.join();

    return 0;
}

But this code can emit an exception, which consequently means that the .join() will not be invoked and the program will crash.

Exception before join

The solution is a Guard class. Let us have a look at it.

#include <iostream>
#include <thread>

class Guard
{
public:    
    Guard(std::thread& t)
        : thread(t)
    { }
    
    ~Guard()
    {
        if (thread.joinable())
        {
            thread.join();
        }
    }
    
    Guard(Guard& other)=delete;
    Guard& operator=(const Guard& rhs)=delete;

private:
    std::thread& thread;
};

void function()
{
    std::cout << "I'm inside function." << std::endl;
}

int main()
{
    std::thread t(function);
    Guard guard(t);

    return 0;
}

An object of the class has a reference to a thread. The destructor checks if the thread is joinable and then joins it. A thread is joinable if .join() or .detach() were not called yet. A guard object can not be copied, because we declared copy constructor and copy assignment operator with =delete. Inability to copy means that the object can not outlive the scope of referenced thread.

This class guarantees that .join() member function will be called in any case. When the function ends, the destructors of all objects in the scope are called before the program exits. This also happens if the function raises an exception and ends prematurely.

Acknowledgment

Thanks to Nino Bašić for comments and suggestions. Now the article looks more organized!

Summary

We learned why are the plain threads dangerous and how to make them safer with a Guard class.

In the next article, we will look how to pass an argument to a thread.

Links: