Как передать std::bind как универсальный ссылочный тип?

Как я понимаю,

  1. std::bind отлично перенаправляет как вызываемый объект, который он обертывает, так и аргументы этому вызываемому объекту;
  2. возвращаемый объект std::bind сам является перемещаемым и/или копируемым, в зависимости от того, являются ли вызываемый объект и его аргументы перемещаемыми и/или копируемыми;
  3. возвращаемый объект std::bind может быть вложенным, и в этом случае внешний возвращаемый объект std::bind можно перемещать и/или копировать, как и при связывании других вызываемых объектов.

Поэтому я ожидаю, что следующий фрагмент кода скомпилируется нормально. Вместо этого код генерирует кучу ошибок компилятора в последних двух операторах в 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!
}

Каждая из двух проблемных строк выдает ошибки без другой. Чтобы устранить все ошибки, я должен закомментировать обе строки.

Вот пример ошибки из g++ 4.9.2 в Cygwin при вызове f() в do_something():

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

Вот пример ошибки из Visual Studio 2013 в той же строке:

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>,> '

Что происходит? Я неправильно понимаю std::bind?

В частности, как я могу

  1. привязать вызываемый объект? и
  2. передать этот возвращаемый объект std::bind функции, принимающей универсальную ссылку? и
  3. вложить этот std::bind возвращаемый объект в другой std::bind?

Моя цель состоит в том, чтобы базовый вызываемый объект и его аргументы были идеально перенаправлены.

EDIT: Чтобы уточнить, я хочу передать обернутый вызываемый объект и его аргументы по значению, а не по ссылке, поэтому std::ref не поможет — по крайней мере, не как полное решение. Причина в том, что мой реальный код более сложен и включает в себя передачу возвращаемого объекта f std::bind через границу потока, а возвращаемые объекты a и b std::bind могут выйти за пределы области действия в исходном потоке до того, как call_handler вызовет f(), поэтому a и b нужно скопировать или переместить в f, а не просто ссылки. Тем не менее, мой вопрос конкретно о std::bind и идеальной пересылке, и для того, чтобы задать хороший вопрос, я выделил все, что не нужно для воспроизведения конкретных ошибок компилятора, о которых я упоминал.


person Craig M. Brandenburg    schedule 09.06.2015    source источник
comment
std::bind принимает аргументы по значению, вы можете использовать std::ref для передачи ссылок   -  person    schedule 09.06.2015
comment
@Dieter Дитер, я отредактировал свой вопрос, чтобы показать, что я специально хочу копировать или перемещать, а не использовать ссылки. Однако, спасибо.   -  person Craig M. Brandenburg    schedule 09.06.2015


Ответы (1)


Ваше предположение 1 неверно, bind всегда передает связанные аргументы в качестве lvalue вызываемому объекту, который он обертывает. Чтобы продемонстрировать это, измените выражение bind в do_something на следующее

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

Следующая строка не сможет скомпилироваться

do_something(std::move(a));

потому что decltype(handler) является ссылкой rvalue, но bind попытается вызвать call_handler со ссылкой lvalue на связанное лямбда-выражение, которое вы передали в main.


Теперь о том, что пошло не так во второй половине вашего примера. bind имеет специальную обработку вложенных bind выражений, которые он распознает и оценивает. Однако в вашем примере вы не хотите, чтобы это произошло. Вместо этого вы хотите, чтобы вложенный bind был перенаправлен как есть в call_handler, который затем вызовет его.

Boost предоставляет boost::protect, который позволяет замаскировать реальный тип вложенного bind и, таким образом, предотвратить его оценку внешним bind.

К сожалению, не существует эквивалента std::protect, но напишите сами.

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));
}

Просто заверните внутреннее выражение bind в protect, и ваш код скомпилируется.

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

Демо

person Praetorian    schedule 09.06.2015
comment
Благодарю вас! Если я правильно понимаю, bind отлично пересылает аргументы вызываемого объекта, но для любых аргументов, которые передаются вызываемому объекту по ссылке, эти аргументы передаются как ссылки lvalue, а не ссылки rvalue — независимо от того, был ли аргумент связан как lvalue или rvalue. - person Craig M. Brandenburg; 10.06.2015
comment
@Craig Нет, bind всегда передает связанные аргументы как lvalue, никогда как rvalue, независимо от того, является ли соответствующий тип параметра в вызываемом объекте ссылкой lvalue/rvalue/forwarding. Вот еще один мой ответ, который показывает безобразие, связанное с получением bind в move связанного аргумента, когда он передает его вызываемому. - person Praetorian; 10.06.2015
comment
Спасибо, вы мне очень помогли. Вот ответ, показывающий обходной путь, аналогичный тому, на который вы ссылались, для получения bind для передачи аргумента в качестве rvalue. В целом, теперь я лучше понимаю, почему Скотт Мейерс советует нам предпочесть лямбда-выражения std::bind. - person Craig M. Brandenburg; 10.06.2015