c++multithreadingconstructormove

c++ thread function accepts class object by value: why is move constructor called?


#include <iostream>
#include <thread>

template<int Num>
class MyClass {
public:
    MyClass(int val) : val_(val) {}

    // Copy constructor
    MyClass(const MyClass& other) : val_(other.val_) {
        std::cout << "Copy constructor called" << std::endl;
    }

    // Move constructor
    MyClass(MyClass&& other) noexcept : val_(other.val_) {
        std::cout << "Move constructor called" << std::endl;
    }

private:
    int val_;
};

template<int Num>
void threadFunction(MyClass<Num> myObj) {
    std::cout << "Inside thread" << std::endl;
}

int main() {
    MyClass<1> obj(42);

    std::thread t1(threadFunction<1>, obj); // <-- cally copy AND move
    std::thread t2(threadFunction<1>, std::ref(obj)); // <-- calls only copy

    t1.join();
    t2.join();

    return 0;
}

I know that std::ref(obj)is actually not necessary in this example based on the answer here: c++ thread function accepting object by value: why does std::ref(obj) compile?

However, different constructors are invoked depending on how obj is passed: copy+move constructor for obj and only copy constructor for std::ref(obj).

Why is that?


Solution

  • Copy and move

    std::thread t1(threadFunction<1>, obj);
    

    Here, the obj is first copied into the std::thread (on the current i.e. main thread) so that when threadFunction<1> is eventually ready to be invoked by the new thread, threadFunction<1> can be called with that copy.

    Note that using this constructor has the effect of

    std::invoke(auto(std::forward<F>(f)), auto(std::forward<Args>(args))...)
    

    The copy is the result of auto(...), which is an rvalue. Then, the move constructor of MyClass is called when std::invoke executes, which passes that rvalue to threadFunction<1>.

    Copy-only

    On the other hand

    std::thread t2(threadFunction<1>, std::ref(obj)); // <-- calls only copy
    

    The thread stores a std::reference_wrapper here instead of copying the object, so the constructor doesn't get called on the main thread. However, eventually, std::invoke is called and calls the copy constructor, as the result of the wrapped reference being passed to the function.

    Better solutions

    If you wanted to prevent copying entirely (at least for the second thread being started) you could write something like:

    std::thread t2([obj = std::move(obj)] {
        threadFunction<1>(std::move(obj));
    });
    

    This would result in two moves, and would mean that obj can go out of scope on the main thread before t2 has started.

    If that's not a safety issue, you could write:

    std::thread t2([&obj] {
        threadFunction<1>(std::move(obj));
    });
    

    This results in only one move.