Наверное, трудно найти такого программиста на C++, который никогда не применял в своем коде boost::bind (с выходом c++11 std::bind). Bind — шаблонная функция, возвращающая обёртку над callable-объектом (т.е. объектом, который можно вызвать, передав ему необходимое число аргументов в круглых скобочках). Bind позволяет изменить сигнатуру вызова такого объекта, сократив число входных аргументов или поменяв какие-то из них местами. Кому интересно, как это может быть реализовано с использованием C++11, прошу под кат.

Примеры использования

Вот простейший (надуманный) пример использования bind-a:

int sum( int lhs, int rhs )
{
  return lhs + rhs;
}

auto f_sum = std::bind( sum, 3, std::placeholders::_2 );
f_sum( 5, 7 ); // f_sum вернет 10

В данном примере std::bind берет за основу указатель на функцию sum и в качестве её аргумента lhs использует 3, а в качестве rhs — второй аргумент из переданных на вход callable-объекта f_sum. Таким образом результатом вызова f_sum будет 10. Все очень просто. Понятно, что подобным же образом std::bind можно использовать и с функциями-членами. Отличие будет только в том, что в этом случае в качестве первого аргумента в конструктор callable-объекта необходимо передать объект соответствующего класса, у которого будет вызвана функция-член.

struct A
{
  void Print() const
  {
    std::cout << "A::Print()" << std::endl;
  }
};

A a;
auto f = std::bind(&A::Print, a);
f(); // "напечатает" A::Print()

Собственно реализация

Итак, std::bind — шаблонная функция, которая принимает на вход указатель на callable-объект и аргументы, которые могут быть константами, переменными или плейсхолдерами (placeholder). В рамках нового (с++11) стандарта это может быть записано так:

namespace naive
{
template<typename Func, typename... BinderArgs>
binder<Func, BinderArgs...> bind( Func const & func, BinderArgs &&... args )
{
  return binder<Func, BinderArgs...>( func, std::forward<BinderArgs>( args )... );
}
}

Здесь binder — шаблонный класс, у которого определен оператор круглые скобки. Помимо этого задача binder-а хранить в себе все переданные ему на вход аргументы. Для простоты реализации делать это он будет по значению. Отметим, что std::bind хранит аргументы также по значению, если его не попросить об ином.

// ...
  template<typename Func, typename... BinderArgs>
  struct binder
  {
    binder( Func const & func, BinderArgs &&... binderArgs )
      : m_func{ func}
      , m_args{ std::forward<BinderArgs>(binderArgs)... }
    {}

    template<typename... Args>
    void operator()( Args &&... args ) const
    {
      // ...
    }
    // ...
  private:
    invoker_t m_invoker;
    Func m_func;
    args_list<BinderArgs...> m_args;
  };
// ...

Здесь naive::args_list очень отдаленно напоминает std::tuple. Его задача хранить произвольное количество аргументов произвольных типов. Понятно, что стандартные контейнеры наподобие std::vector, list, deque,… для этого не годятся. Ниже приводится реализация naive::args_list.

// arg далее используется в качестве базового класса для args_list.
  // дополнительный шаблонный параметр  std::size_t нужен, чтобы имелась возможность различать
  // аргументы с одинаковым типом
  template<std::size_t, typename T>
  struct arg
  {
    explicit arg( T val ) : value( val ) {}

    T const value;
  };
  template<typename,typename...>
  struct args_list_impl;

  template<std::size_t... Indices, typename... Args>
  struct args_list_impl<indices<Indices...>, Args...> : arg<Indices, Args>...
  {
    template<typename... OtherArgs>
    args_list_impl( OtherArgs &&... args ) : arg<Indices, Args>( std::forward<OtherArgs>(args) )... {}
  };

  template<typename... Args>
  struct args_list : args_list_impl< typename make_indices< sizeof...( Args )>::type, Args... >
  {
    using base_t = args_list_impl< typename make_indices< sizeof...( Args ) >::type, Args... >;

    template<typename... OtherArgs>
    args_list( OtherArgs &&... args ) : base_t( std::forward<OtherArgs>(args)... ) {}
  };

Дополнительный шаблонный параметр целого типа структуры naive::arg нужен для того, чтобы различать несколько аргументов с одинаковыми типами(типы одинаковые, индексы — разные). Кстати, аргументы в binder при вызове его оператора «круглые скобки» передаются с использованием args_list (правда, там имеется небольшая хитрость на случай пустого списка).

Движемся далее. В теле класса naive::binder определен оператор круглые скобки, который перенаправляет вызов в соответствующий invoker («вызыватель») в зависимости от переданного на вход callable-объекта. Здесь возможны два варианта: либо была передана «обычная» функция и тогда ее нужно вызвать так (free_function_invoker):

template<typename...Args>
void invoke( Func const & func, Args &&... args ) const
{
  return func( std::forward<Args>(args)... );
}

либо была передана функция-член класса и тогда вызов будет таким (member_function_invoker):

template<typename ObjType, typename...Args>
void invoke( Func const & func, ObjType && obj, Args &&... args ) const
{
  return (obj.*func)( std::forward<Args>(args)... );
}

Какой тип «вызывателя» использовать определяем на этапе конструирования биндера.

using invoker_t =  conditional_t< std::is_member_function_pointer<Func>::value, member_function_invoker, free_function_invoker >;

Теперь самое интересное. Как же реализовать оператор «круглые скобки» у биндера. Сначала посмотрим на код:

template<typename Func, typename... BinderArgs>
struct binder
{
  // ...  
  template<typename... Args>
  void operator()( Args &&... args ) const
  {
    // need check: sizeof...(Args) should not be less than max placeholder value
    call_function( make_indices< sizeof...(BinderArgs) >{}, std::forward<Args>(args)... );
  }

private:
  template< std::size_t... Indices, typename... Args >
  void call_function( indices<Indices...> const &, Args &&... args ) const
  {
    struct empty_list
    {
      empty_list( Args &&... args ) {}
    };

    using args_t = conditional_t< sizeof...(Args) == 0, empty_list, args_list<Args...> >;
    args_t const argsList{ std::forward<Args>(args)... };

    m_invoker.invoke( m_func, take_argument( get_arg<Indices,BinderArgs...>( m_args ), argsList )... );
  }
  // ...
};

Функция get_arg<I, BinderArgs...> просто возвращает I-й элемент из списка args_list<BinderArgs...>, полученного при конструировании naive::binder.

  template<std::size_t I, typename Head, typename... Tail>
  struct type_at_index
  {
    using type = typename type_at_index<I-1, Tail...>::type;
  };

  template<typename Head, typename... Tail>
  struct type_at_index<0, Head, Tail...>
  {
    using type = Head;
  };

  template<std::size_t I, typename... Args>
  using type_at_index_t = typename type_at_index<I, Args...>::type;

  template<std::size_t I, typename... Args>
  type_at_index_t<I, Args...> get_arg( args_list<Args...> const & args )
  {
    arg< I, type_at_index_t<I, Args...> > const & argument = args;
    return argument.value;
  };

Теперь рассмотрим take_arg. Имеется две перегрузки этой функции, одна из которых необходима для работы с плейсхолдерами, другая — для всех остальных случаев. Например, если при создании биндера в его конструктор был передан std::placeholders::_N, то при вызове оператора круглые скобки биндер подставит вместо std::placeholders::_N N-ый аргумент переданный ему на вход. Во всех остальных случаях биндер проигнорирует аргументы, переданные в оператор «круглые скобки» и подставит соответствующие значения, полученные им в конструкторе.

  template<typename T, typename S>
  T take_argument( T const & arg, S const & args )
  {
    return arg;
  }

  template<typename T, typename... Args,
    std::size_t I = std::is_placeholder<T>::value,
    typename = typename std::enable_if< I != 0 >::type >
  type_at_index_t<I-1, Args...> take_argument( T const & ph, args_list<Args...> const & args )
  {
    return get_arg< I-1, Args... >( args );
  }

Вот собственно и всё. Ниже даны примеры использования naive::bind.

Примеры использования
#include "binder.hpp"
#include <iostream>
#include <string>

void Print( std::string const & msg )
{
  std::cout << "Print(): " << msg << std::endl;
}

struct A
{
  void Print( std::string const & msg )
  {
    std::cout << "A::Print(): " << msg << std::endl;
  }
};

int main()
{
  std::string const hello {"hello"};
  auto f = naive::bind( &Print, hello );
  auto f2 = naive::bind( &Print, std::placeholders::_1 );

  f();
  f2( hello );

  A a;
  auto f3 = naive::bind( &A::Print, std::placeholders::_2, std::placeholders::_1 );
  auto f4 = naive::bind( &A::Print, std::placeholders::_1, hello );
  auto f5 = naive::bind( &A::Print, a, std::placeholders::_1 );
  auto f6 = naive::bind( &A::Print, a, hello );

  f3( hello, a );
  f4( a );
  f5( hello );
  f6();

  return 0;
}

Понятно, что naive::bind является наивной (см. название поста) реализацией std::bind и не претендует на включение в стандарт. Многие вещи можно было реализовать по-другому: например, вместо args_list использовать std::tuple, по-иному реализовать invoker (он же в тексте «вызыватель») и т.п. Целью статьи было попытаться разобраться в том, как устроен std::bind под капотом, посмотреть на его простейшую реализацию. Надеюсь, что это получилось. Спасибо за внимание!

» Все исходники можно найти на Github
» Компилировалось всё g++-6.2
Поделиться с друзьями
-->

Комментарии (20)


  1. Mingun
    17.09.2016 23:50
    +1

    Недостает рассказа о том, почему реализация является наивной.


    1. nwwind
      13.01.2017 22:17

      Я немного удивился :)


      1. ellipsis
        18.09.2016 11:07

        Согласен с замечаниями. Учту. Вы верно заметили про std::ref/cref и про кортежи (в терминах статьи это делает реализацию «наивной»).


  1. andy_p
    18.09.2016 09:58

    Приведите, пожалуйста, пример из реальной жизни, когда этот std:: bind действительно нужен.


    1. ellipsis
      18.09.2016 10:58
      +1

      В задачах вызова callback-ов (функциональных объектов, которые необходимо вызвать при условии, что произошло какое-то событие). Часто на практике в качестве callback-объектов используют результат std::(или boost::)bind. Конечно, с выходом с++11 во многих случаях теперь можно обойтись использованием лямбда-функций.


      1. rkfg
        18.09.2016 14:46

        Можно уточнить, что bind нужен не просто для вызова callback'ов, а когда необходимо передать в callback дополнительные параметры. Проблема в том, что сигнатура callback-функции обычно жёстко задана, так что если нужно передать что-то ещё в неё, приходится опираться либо на какие-то переменные в более широком scope (поля класса или даже глобальные переменные, хоть это и фу-фу-фу), либо биндиться. Первый случай имеет ограниченное применение: что если потребуется вызвать два и более одинаковых коллбэка сразу, например, запустить два процесса чтения каких-то данных из сокетов, а по завершении функция чтения вызовет один и тот же коллбэк? Часто нужно как-то различать, откуда именно был вызван коллбэк, чтение какого именно сокета завершено, чтобы отреагировать верно, а переменная у нас лишь одна.

        В этом случае bind спасает, мы делаем коллбэк с бо?льшим числом параметров, чем нужно для стандартной/библиотечной/чужой функции, биндим к нашим собственным параметрам некоторые значения, а те, про которые «знает» чужая функция, заменяем placeholder'ами. Получается такая себе rvalue-функция, которая сигнатурно совпадает с ожидаемой, но при вызове подставит наши заранее привязанные значения к нужным параметрам и вызовет уже нашу расширенную функцию.

        Короче говоря, это такой сигнатурный адаптер, только не для классов, а для функций. По крайней мере, мне в голову не приходят другие применения.


        1. andy_p
          18.09.2016 20:54

          Вообще-то callback функции — это из С. В С++ их заменили виртуальные функции. Кто мешает передать указатель на базовый класс, с виртуальной функцией перекрытой в производном классе?


          1. andy_p
            18.09.2016 20:59
            -2

            Насчет замены аргументов — это вообще жесть. Если взять первый пример в статье, где функция f_sum( 5, 7 ) вернет 10 — то человек, который будет поддерживать вашу программу будет о вас, скажем мягко, не очень высокого мнения.


          1. rkfg
            18.09.2016 21:56
            +1

            Именно так сделано в Java. Но это более громоздко, зачем создавать экземпляр класса, если от него требуется выполнить всего лишь одну функцию? Лишние накладные расходы. А коллбэки понятно оформляются через std::function. Язык, в конце концов, мультипарадигменный, так что функциональный стиль тоже имеет право на жизнь.


          1. Alesh
            19.09.2016 13:03

            Нет callback это не только из С, это из всего)
            Иногда гораздо проще в С++ использовать именно этот подход, чем завязываться на базовый класс в параметре и знание его методов. Во многих случаях это не только лишне, но и тяжело реализовать, например при передачи асинхронного сообщения. зачем вам знать структуру и особенности реализации (объектной иерархии в частности) получателя события? Вам дали callback, дергайте за него «и не задавайте лишних вопросов») Получатель события сам сформирует и подготовит правильный callback, и вот тут как раз пригодится bind или анонимная лямбда. Что лучше из этого, вопрос дискуссионный) наверно надо в каждом конкретном случае решать отдельно.


      1. gasizdat
        18.09.2016 17:51

        А чем плох вариант оборачивания в анонимную лямбду? ИМХО, на порядок нагляднее всяких байндов с плейсхолдерами. По производительности — одинаково. По памяти скорее всего тоже.


        1. nolane
          19.09.2016 08:49
          +1

          С чего вы взяли? Нельзя просто так такими заявлениями бросаться — вам ведь могут поверить. Надо измерять.
          Вот, Stephan T. Lavavej, с вами не согласен.


          1. VioletGiraffe
            19.09.2016 08:58

            Окей, в теле «горячего» цикла лучше использовать bind, но обычно читаемость предпочтительна микроскопическому выигрышу в производительности там, где это не нужно.
            Медленно работающая программа лучше быстрой, но глючной.


            1. VioletGiraffe
              19.09.2016 09:09

              P. S. Сам STL в этом видео советует не использовать bind.


            1. nolane
              19.09.2016 18:10

              Я хотел сказать, что по производительности вовсе не одинаково. Лучше использовать лямбды.


  1. VioletGiraffe
    18.09.2016 16:21
    +2

    Вот сколько пользовался functional — а bind ни разу не понадобился. Можно же всё в лямбду завернуть, читаемость и простота написания на порядок выше.


    1. AxisPod
      19.09.2016 08:52
      +2

      Ну во-первых boost::bind был во времена, когда лямбд еще не было, а во-вторых, вот когда напишите свою сетевую работу на boost::asio, тогда поймете.


  1. iperov
    18.09.2016 17:41
    -2

    трудно найти такого программиста на C++, который никогда не применял в своем коде boost::bind

    в вашем ареале обитания?


    1. ellipsis
      18.09.2016 20:12
      +1

      Не совсем понятно, почему в этой цитате Вы опустили слово «Наверное, ...». Извините, если это предложение Вас как-то задело.


      1. tangro
        19.09.2016 00:55

        Ну, в общем это неверное предположение. Можно годами счастливо писать на С++ без bind.