Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is it impossible to move a variable to another std::thread

What is the reason that you can't move an object to another std::thread? There are situations where it could be useful. For example:

You create a loop that accepts incoming socket connections. It would be nice to move incoming connections to another thread that will handle the connections. You don't need the connection anymore in the accept loop. So why should you create a pointer?

A small test case:

#include <iostream>
#include <thread>

using namespace std;

class Pointertest
{
public:
    Pointertest() {cout << "Constructor";}
    Pointertest(Pointertest &pointertest) {cout << "Copy";}
    Pointertest(Pointertest &&pointertest) {cout << "Move";}
    ~Pointertest() {cout << "Destruct";}
};

void foo(Pointertest &&pointertest)
{

}

int main()
{
    Pointertest pointertest;

    foo(std::move(pointertest)); //Works
    thread test(foo,std::move(pointertest)); //**cannot convert parameter 1 from 'Pointertest' to 'Pointertest &&'**
}
like image 581
Ordo Avatar asked May 13 '13 17:05

Ordo


2 Answers

The std::thread constructor has to treat the arguments you give it somewhat differently from most forwarding functions.

The reason for this is due to questions of when the thread actually gets started. If the part of the function invocation that actually created the function argument gets run long after the thread object is created (which is entirely legal behavior), then the object that needs to be moved from may have long since been destroyed.

Just consider an altered version of your code:

std::thread some_func()
{
    Pointertest pointertest;

    thread test(foo,std::move(pointertest));
    return test;
}

This is perfectly valid (the thread will be moved out of the function). However, there's a big problem. foo may not have been called yet. And since foo takes its parameter by reference, it now has a reference to a stack variable that has been destroyed.

That's bad. But even if foo took its parameter by value, it would change nothing. Because the actual movement into that parameter doesn't happen until some indeterminate time after the thread has been started. The attempt to move into the parameter will still use an rvalue reference to a stack variable that has been destroyed. Which again is bad.

Therefore, std::thread constructor does something different. It copy/moves the arguments you give it into internal storage (this is done on the current thread). Then it uses those values as arguments for the actual function call (this is done on the new thread).

According to the standard, the thread constructor should treat pass these internal variables to your functions as temporaries. The standard specifically states INVOKE (DECAY_COPY ( std::forward<F>(f)), DECAY_COPY (std::forward<Args>(args))...), where the DECAY_COPY stuff happens on the original thread, while the INVOKE part happens on the new thread.

So it seems like your thread implementation isn't able to forward the non-copyable parameters through correctly. You should be able to pass a non-copyable type; the arguments are only required to be MoveConstructible.

So this would appear to be a bug in your implementation.

like image 165
Nicol Bolas Avatar answered Sep 28 '22 04:09

Nicol Bolas


It is possible. Fixing the signature of your copy constructor makes it work for me:

class Pointertest
{
public:
    Pointertest() {cout << "Constructor";}
    Pointertest(Pointertest const& pointertest) {cout << "Copy";}
//                          ^^^^^^
    Pointertest(Pointertest &&pointertest) {cout << "Move";}
    ~Pointertest() {cout << "Destruct";}
};

Also, do not forget to join your thread (or detach from it) before your thread object goes out of scope:

int main()
{
    Pointertest pointertest;
    thread test(foo, std::move(pointertest));

    test.join();
//  ^^^^^^^^^^^^
}
like image 42
Andy Prowl Avatar answered Sep 28 '22 05:09

Andy Prowl