C++ method in thread. Difference between passing: object, object's address, std::ref of object

Refresh

April 2019

Views

129 time

1

I am trying to execute an object's method in a C++ thread.

I am able to do it, by passing the method's address and the object (or the object's address, or std::ref(my_obj)) to the thread's constructor.

I observed that if I pass the object, rather than the object's address or std::ref(my_obj), then the object gets copied twice (I'm printing some info in the copy constructor to see that).

Here is the code:

class Warrior{
    string _name;
public:
    // constructor
    Warrior(string name): _name(name) {}

    // copy constructor (prints every time the object is copied)
    Warrior(const Warrior & other): _name("Copied " + other._name){
        cout << "Copying warrior: \"" << other._name;
        cout << "\" into : \"" << _name << "\"" << endl;
    }

    void attack(int damage){
        cout << _name << " is attacking for " << damage << "!" << endl;
    }
};

int main(){
    Warrior conan("Conan");

    // run conan.attack(5) in a separate thread
    thread t(&Warrior::attack, conan, 5);
    t.join(); // wait for thread to finish

}

The output I get in this case is

Copying warrior: "Conan" into : "Copied Conan"
Copying warrior: "Copied Conan" into : "Copied Copied Conan"
Copied Copied Conan is attacking for 5!

While if I simply pass &conan or std::ref(conan) as a second argument to thread t(...) (instead of passing conan), the output is just:

Conan is attacking for 5!

I have 4 doubts:

  1. Why is that I have 2 copies of the object instead of 1?

    I was expecting that by passing the instance of the object to the thread's constructor, the object would get copied once in the thread's own stack, and then the attack() method would be called on that copy.

  2. What is the exact reason why the thread's constructor can accept an object, an address, or a std::ref? Is it using this version of the constructor (which I admit I do not fully understand)

    template< class Function, class... Args > explicit thread( Function&& f, Args&&... args );

    in all 3 cases?

  3. If we exclude the first case (since it's inefficient), what should I use between &conan and std::ref(conan)?

  4. Is this somehow related to the syntax required by std::bind?

1 answers

4

Why is that I have 2 copies of the object instead of 1?

When you spin up a thread the parameters are copied into the thread object. Those parameters are then copied into the actual thread that gets created, so you have two copies. This is why you have to use std::ref when you want to pass parameter that the function takes by reference.

What is the exact reason why the thread's constructor can accept an object, an address, or a std::ref? Is it using this version of the constructor (which I admit I do not fully understand)

std::thread basically starts the new thread with a call like

std::invoke(decay_copy(std::forward<Function>(f)), 
            decay_copy(std::forward<Args>(args))...);

std::invoke is built to handle all different sorts of callables and one of those is when it has a member function pointer and an object, and it calls the function appropriately. It also knows about std::reference_wrapper and can handle calling a pointer to a member function on a std::reference_wrapper to an object.

If we exclude the first case (since it's inefficient), what should I use between &conan and std::ref(conan)?

This is primarily opinion based. They both essentially do the same thing, although the first version is shorter to write.

Is this somehow related to the syntax required by std::bind?

Kind of. std::bind's operator() is also implemented using std::invoke so they have a very common interface.


All of that said you can use a lambda to give yourself a common interface.

thread t(&Warrior::attack, conan, 5);

can be rewritten as

thread t([&](){ return conan.attack(5); });

And you can use this form for pretty much any other function you want to call. I find it is easier to parse when seeing a lambda.