3

The function signature for std::bind() is as follows:

template< class F, class... Args >
/*unspecified*/ bind( F&& f, Args&&... args );

So args is a variadic template universal reference, if I understand correctly. The traditional template type deduction rules for universal references are as follows:

  1. If the argument passed to T is an lvalue, then T is deduced to an lvalue reference
  2. If the argument passed to T is an rvalue, then T is deduced to an lvalue

... and I think these rules would apply to each arg in args individually, meaning that all lvalues passed into std::bind() as an argument to the functor would be passed by reference. However this contradicts the program below:

#include <iostream>
#include <functional>

void function(int& n) {
  n++;
}

int main() {
    int n = 0;
    auto functor = std::bind(function, n);
    functor();
    std::cout << n << std::endl; // 0, not 1.
    return 0;
}

In order to get n to be passed by reference, you must do so explicitly via std::ref(n), which really confuses me given what (little) I know about universal references and perfect forwarding. How does std::bind() take anything by value when it uses universal references, which would otherwise consume lvalues as references?

Dominic Farolino
  • 1,362
  • 1
  • 20
  • 40

1 Answers1

3

It has almost nothing to do with the signature, it is a design choice. std::bind must of course store all its bound arguments somehow and it stores them as values. The "universality" is only used to properly construct them - by move or copy.

std::ref is also stored by value but due to its nature, the wrapped object is "stored" by reference.

std::thread has exactly the same behaviour. One can argue that (move) constructing a copy by default is safer because both returned objects tend to outlive locals which are the likeliest to be captured.

Quimby
  • 17,735
  • 4
  • 35
  • 55
  • I see, I think it makes sense. I tried my hand at making a bind() impl, and stored the arguments in a tuple which I directly forwarded the arguments into. I think this means the tuple ended up storing _references_ instead of values which is why I found it difficult to achieve the same value semantics as std::bind(). I’m curious how I would achieve the same value semantics as std::bind though. The only thing I can imagine would be using std::remove_reference<...> on each arg, so that the tuple is always constructed with values, each either move or copy constructed, depending on the forwarding? – Dominic Farolino Sep 06 '21 at 06:33
  • @DominicFarolino Actually the cppref link you posted covers all types in detail - objects are stored as `e std::decay::type` so something like `std::tuple::type...> m_storage{std::forward(args)...);`. Very similar to what you had in mind, `decay` is just more powerful - removes `const` and `volatile`. – Quimby Sep 06 '21 at 07:18
  • Thanks this makes a lot of sense. I now see how to actually store things correctly and safely in the tuple... if you're feeling up for it, you can check out this question where I ask how to actually invoke the functor on move-only types, since I can't quite figure out how to get this right: https://stackoverflow.com/questions/69078145/ – Dominic Farolino Sep 06 '21 at 19:13