c++ thread function accepting object by value: why does std::ref(obj) compile?
Asked Answered
H

1

0
#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); // <-- pass by value
    std::thread t2(threadFunction<1>, std::ref(obj)); // <-- pass by reference

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

    return 0;
}

I want to pass an object by value to a thread to ensure each thread works on its own copy (thread t1).

When I create thread t2, I pass the object by reference using std::ref(obj).

Clearly, my intention per function definition is to pass the object by value rather than by reference. Is obj a shared variable if passed as std::ref(obj) among all threads?

This question std::ref using with template function and function passing by value seems to address a similar topic, but not in multithreading context. In the example shown there, there seems to be a difference if the function is templated.

EDIT 1:

I added a copy and move constructor to the class motivated by the discussion in the answer below.

Hittel answered 26/4, 2024 at 6:54 Comment(2)
The same reason you can assign a reference to an object? Having threads involved doesn't make any difference, you'd see the same behaviour without godbolt.org/z/8zhPn6W81Foggy
@AlanBirtles, what do you mean by "assign a reference to an object?" Do you mean that you can write a=b where the type of a is T and the type of b is T&? Or, do I misunderstand you? I would call a=b an assignment of a value. The value is obtained from the location to which b refers, and it is assigned to (i.e., copied to) the location that is named by a.Bertberta
S
5

Why would it not compile?

std::ref returns a std::reference_wrapper which has an implicit conversion to reference to stored object that is used here.

Is obj a shared variable if passed as std::ref(obj) among all threads?

No. Each thread gets a copy.

The situation is similar to

 void foo( int a) { std::cout << a; }

 int main() {
     int x = 42;
     int& ref = x;
     foo(ref);
 }

ref is a reference to x. When you call foo(ref) then a is a copy of x.

Actually the need to use std::ref arises because the thread constructor always gets its parameters by value. And if the function called by the thread is template <typename Num> void threadFunction(MyClass<Num> myObj), ie it accepts a value, then nothing can magically change it to get myObj passed by reference.

Selemas answered 26/4, 2024 at 7:3 Comment(11)
That said, when a thread function accepts its parameters by value, std::ref(arg) has no effect?Hittel
@Hittel what do you think? What effect can it have?Selemas
Honestly, I am not sure. Is it more a performance issue?Hittel
@Hittel its what I wrote in the answer. std::ref returns a std::reference_wrapper around the object, this is implicitly converted to a reference to the object, from that a copy is made. It is as-if you did not use std::ref. if you turn it on the optimizer will most likely notice that. However, imho "is there a performance issue" is the wrong question because using std::ref when there is no need to is not simple code. It confuses the reader, they might think it is pass by reference.Selemas
I see. I observed the following by adding Copy+Move constructor to the class. std::thread(threadFunction, obj)call both Copy+Move constructor, whereas std::thread(threadFunction, std::ref(obj)) calls only Copy constructor. I only have basis knowledge about that, but I think moving (large) objects is cheap compared to copying, right?Hittel
@Hittel I can comment on the code with intrumented constructors if you show it.Selemas
@Hittel the size alone is not what determines the performance difference between a copy and a move. Moving an int is exactly the same as copying it. If your class has only int members there is no differenceSelemas
I edited my question by adding a move and copy constructor and they will be invoked as I told in my previous comment. Maybe you can comment / expand your answer.Hittel
@Hittel I think I can confirm what you say. Looks interesting godbolt.org/z/3MjYqn68P. You can open another question about it. Please do not change the question to ask for something else after you received ansewrs. My answer is for the question in its initial state.Selemas
@Hittel note that how many copies or moves are made does not change the answer to your original question. Each thread gets a copy.Selemas
FYI I opened a new question concerning the constructors: #78389697Hittel

© 2022 - 2025 — McMap. All rights reserved.