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

1

5
#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?

Rase answered 26/4, 2024 at 8:35 Comment(5)
OT: Use std::ref (or std::cref) when the thread function takes its argument by reference. Otherwise it doesn't really make sense.Vadnee
What you see is the thread object creating a copy, and then moving it for the thread-function call. This is the reason e.g. std::ref is needed to pass objects by reference, because the thread object creates a copy, and references can't be copied.Vadnee
I know, but it makes no difference here. Do you have an idea as to why the different constructors are called?Rase
But if we can skip the move operation (using std::ref(obj)) is it then not the better solution in terms of efficiency? of course, it may look confusing to use std::ref() for an object passed by value, but thats a different questionRase
Depending on your needs, you could use std::move(obj) as argument, then a single "move" should be the only thing happening.Vadnee
B
11

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.

Brucie answered 26/4, 2024 at 8:53 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.