DEV Community

Cover image for Destruction order vs thread safety in C++
pikoTutorial
pikoTutorial

Posted on • Originally published at pikotutorial.com

Destruction order vs thread safety in C++

Welcome to the next pikoTutorial !

What's the problem?

Let's take a look at the simple example below:

#include <iostream>
#include <future>
#include <thread>
#include <memory>
// define a class spinning up a thread
class SomeClass
{
public:
    SomeClass()
    : value_(std::make_unique<int>(12)) 
    {
        // spin up a separate thread
        future_ = std::async([this](){
            // wait for 1 second before executing any actions in the thread
            std::this_thread::sleep_for(std::chrono::seconds(1));
            std::cout << "Starting a separate thread!" << std::endl;
            std::cout << "Value: " << *value_ << std::endl;
        });
    }

    std::future<void> future_;
    std::unique_ptr<int> value_;
};

int main(int argc, char** argv)
{
    SomeClass obj;
}
Enter fullscreen mode Exit fullscreen mode

Let's quickly sum up what's going on here and what could be the expected output of such program:

  • we have a class which holds a pointer to an integer value and which is allocated and set to 12 on the constructor's initializer list
  • the class spins up a thread in the constructor using std::async and stores its result in a std::future, so that the std::async call does not block the code execution, but only start the thread and move forward instead
  • the created thread will always have a chance to finish because std::future calls get() in its destructor, so as soon as our class is being destroyed, the destructor of the member variable future_ also gets called which ends up in a blocking get() call which waits for the associated thread to finish
  • the thread informs us when it starts and prints the value that the member pointer points to (in this case 12) Nothing special here, but when we run this application we get the following output:
Starting a separate thread!
Segmentation fault (core dumped)
Enter fullscreen mode Exit fullscreen mode

To understand why it happens, let's add a custom deleter to the unique pointer, just to be able to print something at the moment when value_ gets destroyed. I'll add also a debug print to the destructor of SomeClass:

Note: I omitted all the standard includes to save on vertical space.

// define a custom deleter
void custom_int_deleter(int* ptr)
{
    std::cout << "Destroying int pointer..." << std::endl;
    delete ptr;
}
// define a class spinning up a thread
class SomeClass
{
public:
    SomeClass()
    : value_(new int(12), &custom_int_deleter) 
    {
        // spin up a separate thread
        future_ = std::async([this](){
            // wait for 1 second before executing any actions in the thread
            std::this_thread::sleep_for(std::chrono::seconds(1));
            std::cout << "Starting a separate thread!" << std::endl;
            std::cout << "Value: " << *value_ << std::endl;
        });
    }
    ~SomeClass() { std::cout << "Destroying SomeClass..." << std::endl; }

    std::future<void> future_;
    std::unique_ptr<int, void(*)(int*)> value_;
};

int main(int argc, char** argv)
{
    SomeClass obj;
}
Enter fullscreen mode Exit fullscreen mode

Note for beginners: If you don't like function pointer syntax in the unique pointer declaration, you can replace void(*)(int*) with decltype(&custom_int_deleter). Just remember, that it's not just a matter of personal preferences, but a matter of value which each of these ways brings. The first one abstracts you from the function name, the second one abstracts you from the function signature. The signature of the deleter is always void(T*) and that's why I chose in the example to abstract from the deleter's name.

Now there's still a segmentation fault, but the output gives us more information of what's going on:

Destroying SomeClass...
Destroying int pointer...
Starting a separate thread!
Segmentation fault (core dumped)
Enter fullscreen mode Exit fullscreen mode

It looks like the pointer gets destroyed before the thread starts what means that in line 19 we're dereferencing a deleted pointer!

The reason behind this behavior is the order of destruction of the class member variables - the same as in every other scope in C++, the member variables are destroyed in the reverse order of how they were created. By default, the construction order is just the order of the members in the class definition, so in case of our example it's the following:

std::future<void> future_;
std::unique_ptr<int, void(*)(int*)> value_;
Enter fullscreen mode Exit fullscreen mode

What means that if value_ is created after the future_, it will be destroyed before the destructor of future_ gets called, potentially allowing the associated thread to outlive the member variables which it may be using before finishing - exactly as in our example. The fix for that may be as simple as changing the order of the members in the class definition:

std::unique_ptr<int, void(*)(int*)> value_;
std::future<void> future_;
Enter fullscreen mode Exit fullscreen mode

After that, the destructor of future_ will wait in a blocking way for the thread to finish before other members are destroyed:

Destroying SomeClass...
Starting a separate thread!
Value: 12
Destroying int pointer...
Enter fullscreen mode Exit fullscreen mode

When it happens in real life?

The above example was pretty easy to debug because we were explicitly using a resource after it has been explicitly deleted, but remember that in real life such kind of bugs may be much more tricky to track down because your thread's code may look more like this:

future_ = std::async([this](){
    std::lock_guard<std::mutex> lock(mtx_);
    // lots of code and some time-consuming processing
});
Enter fullscreen mode Exit fullscreen mode

In this example you can say "Lock guard locks mutex at the beginning of the thread, so I'm fairly sure that the mutex will be valid at that point of time". However, in the above example it's not the locking which is the problematic part - it's the unlocking. See, the std::lock_guard holds a reference to the mutex which is then locked in the lock guard's constructor and unlocked in the destructor. Are you sure that by the time the thread scope exits and lock guard attempts to unlock the mutex, the mutex will still be there?

Dev Diairies image

User Feedback & The Pivot That Saved The Project

🔥 Check out Episode 3 of Dev Diairies, following a successful Hackathon project turned startup.

Watch full video 🎥

Top comments (0)

Redis image

Short-term memory for faster
AI agents 🤖💨

AI agents struggle with latency and context switching. Redis fixes it with a fast, in-memory layer for short-term context—plus native support for vectors and semi-structured data to keep real-time workflows on track.

Start building

👋 Kindness is contagious

Discover fresh viewpoints in this insightful post, supported by our vibrant DEV Community. Every developer’s experience matters—add your thoughts and help us grow together.

A simple “thank you” can uplift the author and spark new discussions—leave yours below!

On DEV, knowledge-sharing connects us and drives innovation. Found this useful? A quick note of appreciation makes a real impact.

Okay