Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

By-ref arguments: is this an inconsistency between std::thread and std::bind?

Tags:

c++

c++11

std::bind and std::thread share a few design principles. Since both of them store local objets corresponding to the passed arguments, we need to use either std::ref or std::cref if reference semantics is desired:

void f(int& i, double d) { /*...*/ }

void g() {
    int x = 10;
    std::bind(f, std::ref(x), _1) (3.14);
    std::thread t1(f, std::ref(x), 3.14);
    //...
}

But I'm intrigued by a recent personal discovery: std::bind will allow you to pass a value in the case above, even though this is not what one usually wants.

    std::bind(f, x, _1) (3.14); // Usually wrong, but valid.

However, this is not true for std::thread. The following will trigger a compile error.

    std::thread t2(f, x, 3.14); // Usually wrong and invalid: Error!

At first sight I thought this was a compiler bug, but the error is indeed legitimate. It seems that the templated version of std::thread's constructor is not able to correctly deduce the arguments due to the copy decaying requirement (tranforming int& in int) imposed by 30.3.1.2.

The question is: Why not require something similar to std::bind's arguments? Or is this apparent inconsistency intended?

Note: Explained why it's not a duplicated in the comment below.

like image 494
Leandro T. C. Melo Avatar asked May 04 '17 17:05

Leandro T. C. Melo


People also ask

What will happen if passing reference through std :: thread?

If you have used std::thread or std::bind , you probably noticed that even if you pass a reference as parameter, it still creates a copy instead. From cppreference, The arguments to the thread function are moved or copied by value.

Can you pass a reference to a thread C++?

In c++11 to pass a referenceto a thread, we have std::ref(). std::thread t3(fun3, std::ref(x)); In this statement we are passing reference of x to thread t3 because fun3() takes int reference as a parameter.

What does std :: ref do?

An std::reference_wrapper is a copyable and assignable object that emulates a reference. Contrary to its name, it does not wrap a reference. It works by encapsulating a pointer (T*) and by implicitly converting to a reference (T&).


3 Answers

The function object returned by bind is designed for reuse (i.e., call be called multiple times); it therefore must pass its bound arguments as lvalues, because you don't want to move from said arguments or later calls would see a moved-from bound argument. (Similarly, you want the function object to be called as an lvalue, too.)

This concern is inapplicable to std::thread and friends. The thread function will only be called once, with the provided arguments. It's perfectly safe to move from them because nothing else is going to be looking at them. They are effectively temporary copies, made just for the new thread. Thus the function object is called as an rvalue and the arguments are passed as rvalues.

like image 146
T.C. Avatar answered Oct 22 '22 14:10

T.C.


std::bind was mostly obsolete when it arrived due to the existence of lambdas. With C++14 improvements and C++17 std::apply, the remaining use cases for bind are pretty much gone.

Even in C++11 the cases where bind solved a problem that a lambda did not solve where relatively rare.

On the other hand, std::thread was solving a slightly different problem. It doesn't need the flexibility of bind to "solve every issue", instead it could block what would usually be bad code.

In the bind case The reference passed to f won't be x but rather a reference to an internal stored copy of x. This is extremely surprising.

void f(int& x) {
    ++x;
    std::cout << x << '\n';
};

int main() {
    int x = 0;
    auto b = std::bind(f, x);
    b();
    b();
    b();
    std::cout << x << '\n';
}

prints

1
2
3
0

where the last 0 is the original x, while 1 2 and 3 are the incremented copy of x stored within f.

With lambda, the difference between a mutable stored state and an external reference can be made clear.

auto b = [&x]{ f(x); };

vs

auto b = [x]()mutable{ f(x); };

one of which copies x then invokes f repeatedly on it, the other passes a reference to x into f.

There really isn't a way to do this with bind without allowing f to access the stored copy of x as a reference.

For std::thread, if you want this mutable local copy behavior you just use a lambda.

std::thread t1([x]()mutable{ f(x); });

In fact, I would argue most of the INVOKE syntax in C++11 seems to be a legacy of not having C++14 power lambdas and std::apply in the language. There are very few cases not solved by lambda and std::apply (apply is needed as lambdas don't easily support moving packs into them then extracing them inside).

But we don't have a time machine, so we have these multiple parallel ways to express the idea of invoking something in a specific context in C++.

like image 25
Yakk - Adam Nevraumont Avatar answered Oct 22 '22 14:10

Yakk - Adam Nevraumont


From what I can tell, thread started off with basically the same rules as bind, but was modified in 2010 by N3090 to take on the constraint you've identified.

Using that to bisect the various contributions, I believe you're looking for LWG issue 929.

Ironically, the intention seems to have been to make the thread constructor less constrained. Certainly there's no mention of bind, although this wording was later also applied to async ("Clean-up" section after LWG 1315), so I would say bind got left behind.

It's quite hard to be sure, though, so I would recommend asking the committee itself.

like image 45
Lightness Races in Orbit Avatar answered Oct 22 '22 14:10

Lightness Races in Orbit