How to pass std::bind as universal reference type?

979 views Asked by At

As I understand it,

  1. std::bind perfectly forwards both the callable object it wraps and the arguments to that callable object;
  2. the std::bind return object is itself movable and/or copyable, depending on whether the callable object and its arguments are movable and/or copyable;
  3. a std::bind return object may be nested, in which case the outer std::bind return object is movable and/or copyable, just as when binding other callable objects.

Therefore, I would expect the following code snippet to compile okay. Instead, the code generates a spew of compiler errors at the last two statements in main().

#include <functional>

template<typename HandlerType>
void call_handler(HandlerType&& handler)
{
  handler();
}

template<typename HandlerType>
void do_something(HandlerType&& handler)
{
  auto f = std::bind(
    &call_handler<HandlerType&>,
    std::forward<HandlerType>(handler));
  f();
}

int main()
{
  auto a = [&]() {};
  do_something(a);
  do_something(std::move(a));

  auto b = std::bind([&]() {});
  do_something(b);              // <- compiler error!
  do_something(std::move(b));   // <- compiler error!
}

Each of the two problem line spews errors without the other. To eliminate all errors, I must comment out both lines.

Here's a sample error, from g++ 4.9.2 in Cygwin, at the call to f() in do_something():

(4 of 103): error: no match for call to ‘(std::_Bind<void (*(std::_Bind<main()::<lambda()>()>))(std::_Bind<main()::<lambda()>()>&)>) ()’

Here's a sample error from Visual Studio 2013, at the same line:

1>C:\Program Files (x86)\Microsoft Visual Studio12.0\VC\include\functional(1149): error C2664: 'void (HandlerType)' : cannot convert argument 1 from 'void' to 'std::_Bind<false,void,main::<lambda_2b8ed726b4f655ffe5747e5b66152230>,> '

What's going on? Do I misunderstand std::bind?

Specifically, how can I

  1. bind a callable object? and
  2. pass that std::bind return object to a function taking a universal reference? and
  3. nest that std::bind return object in another std::bind?

My goal is for the underlying callable object and its arguments to be perfectly forwarded.

EDIT: To clarify, I want to pass the wrapped callable object and its arguments by value, not by reference, so std::ref won't help—at least, not as a full solution. The reason is that my real code is more complex and involves passing the f std::bind return object across a thread boundary, and both the a and b std::bind return objects may go out of scope in the original thread before call_handler calls f(), so a and b need to copy or move into f, not be mere references. That said, my question is specifically about std::bind and perfect forwarding, and for the purpose of asking a good question, I've distilled out everything not needed to reproduce the specific compiler errors I mentioned.

1

There are 1 answers

3
Praetorian On BEST ANSWER

Your assumption 1 is wrong, bind always passes the bound arguments as lvalues to the callable it is wrapping. To demonstrate this, change the bind expression within do_something to the following

auto f = std::bind(
    &call_handler<decltype(handler)>,
    std::forward<HandlerType>(handler));

The following line will then fail to compile

do_something(std::move(a));

because decltype(handler) is an rvalue reference, but bind will try to call call_handler with an lvalue reference to the bound lambda expression you passed it in main.


Now for what's going wrong in the second half of your example. bind has special handling for nested bind expressions, which it will recognize and evaluate. However, in your example, you don't want that to happen. Instead, you want the nested bind to be forwarded as is to call_handler, which will then invoke it.

Boost provides boost::protect which lets you mask the real type of the nested bind and thus prevent its evaluation by the outer bind.

Unfortunately, there is no std::protect equivalent, but it's not difficult to write it yourself.

template<typename T>
struct protect_wrapper : T
{
    protect_wrapper(const T& t) : T(t)
    {}

    protect_wrapper(T&& t) : T(std::move(t))
    {}
};

template<typename T>
std::enable_if_t<!std::is_bind_expression<std::decay_t<T>>::value,
                 T&&
                >
protect(T&& t)
{
    return std::forward<T>(t);
}

template<typename T>
std::enable_if_t<std::is_bind_expression<std::decay_t<T>>::value,
                 protect_wrapper<std::decay_t<T>>
                >
protect(T&& t)
{
    return protect_wrapper<std::decay_t<T>>(std::forward<T>(t));
}

Just wrap your inner bind expression with protect, and your code will compile.

auto b = protect(std::bind([&]() {}));
do_something(b);
do_something(std::move(b));

Live demo