1 апр. 2009 г.

Использование boost::bind

Одним из самых мощных средств стандартной библиотеки языка являются алгоритмы. Их в STL много. А там, где алгоритмы – появляются функторы и их разновидности – предикаты. И все было бы замечательно, если бы не одна большая ложка дегтя – средства стандартной библиотеки предоставляют слишком мало возможностей для описания функуторов. Все просто, если в алгоритм необходимо передать какую-либо глобальную функцию, или простой указатель на метод класса, хранящегося в контейнере.

Но когда речь заходит о чем-то более сложном, как-то:

  • передача заданного значения в предикат;
  • передача указателя на метод класса, не являющегося членом контейнера;
  • предварительная обработка элементов контейнера перед передачей их в функтор;
  • передача в функтор значений элементов данных объектов обрабатываемой последовательности;
  • (список можно продолжить…)

то STL уже не может предложить чего-либо более-менее пригодного к использованию. Сразу же оказывается, что std::binder1st и std::binder2nd накладывают существенные ограничения на принимаемые параметры (кто не сталкивался с «reverence to reference is not allowed» при использовании их в связке с mem_fun_ref?), в стандартные предикаты нельзя передать результат выполнения некоторой операции над элементом контейнера, std::mem_fun и std::mem_fun_ref позволяют работать только с объектами, являющимися членами обрабатываемой последовательности… Вообщем мрак. Проблемы некоторым образом решаются с помощью написания собственных функторов и адаптеров функторов. Но разработка минимально необходимого джентльменского набора – сама по себе нетривиальная задача. Тем более, что она уже имеет весьма элегантное решение, называемое boost::bind. Без преувеличения можно сказать, что по сравнению со связкой boost::bind/boost::mem_fn близкие по функциональности классы из STL кажутся жалкой поделкой. И не без основания. При использовании классов из STL приходится всякий раз задумываться – а какой из вариантов связывателя подойдет к данному конкретному случаю, и подойдет ли вообще. С boost::bind о подобных вещах заморачиваться не надо. К каждому варианту использования компилятор самостоятельно подберет необходимый связыватель. Или выдаст ошибку, если соответствующего варианта подобрать нельзя. В коде использовать этот класс не просто, а очень просто. Рассмотрим несколько типовых вариантов.

Использование с глобальной функцией

Предположим, что есть функция:

bool compare(int a, int b) {return a < b;}

для того, чтобы использовать эту функцию в, например, алгоритме sort нам необходимо написать:

std::vector<int> vec(…);
std::sort(vec.begin(), vec.end(), compare);

Теперь немного усложним задачу – в зависимости от флага нам необходимо сортировать либо по возрастанию, либо по убыванию. При использовании только STL пришлось бы писать две функции. Накладно. С помощью boost эту задачу решить значительно проще:

bool compare(int a, int b, bool reverseSort) {return reverseSort ? a > b : a < b;}

 
std::vector<int> vec(…);
std::sort(vec.begin(), vec.end(), boost::bind(compare, _1, _2, order));

где order – булевская переменная, задающая порядок сортировки. Последняя строчка выглядит несколько странновато, но именно в ней и заключается вся суть. В результате ее компиляции будет получен код, реализующий сортировку массива, использующий для сравнения функцию compare, и передающей ей на вход три аргумента – два числа, которые необходимо сравнить, и способ их сравнения. Почему именно так? Рассмотрим этот код подробнее, записав его так:

int a = 1, b = 2;
boost::bind(compare, _1, _2, false)(a, b);

(что-то подобное содержится в недрах функции std::sort). Последняя строчка в этом коде обозначает следующее:

boost::bind – функция, создающая необходимый нам связыватель. Возвращает экземпляр функционального объекта (функтора), для которого применим оператор вызова функции. (compare, - первый аргумент функции определяет имя/указатель на функцию, которая должна быть вызвана в связывателе. В данном случае это функция compare; _1, _2 – плейсхолдеры, определяющие логику передачи параметров из оператора вызова функции в функцию compare. , false) – значение последнего аргумента, передаваемого в compare. (a, b) – оператор вызова функции, производящий связывание аргументов и вызов функции compare. В итоге будет выполнена следующий код:

compare(a, b, false);

Из приведенного примера можно увидеть, что упомянутые выше плейсхолдеры определяют связь между аргументами у оператора вызова функции связывателя и аргументами вызываемой функцией. Причем, количество аргументов у оператора вызова функции связывателя определяется количеством плейсхолдеров:

boost::bind(compare, _1, 4, false)(a, b); // неправильно. 

boost::bind(compare, _1, 4, false)(a); // правильно
boost::bind(compare, _1, _2, false)(a); // неправильно

boost::bind(compare, _1, _2, false)(a, b); // правильно

Очевидно, что попытка задать в вызове boost::bind количество параметров, несоответствующее количеству обязательных параметров в вызываемой функции приведет к ошибке. Равно как и попытка указания количества параметров большего, чем количество параметров в вызываемой функции. Действуя по аналогии мы можем записать:

std::find_if(vec.begin(), vec.end(), boost::bind(compare, _1, 4, false)); // поиск первого числа, меньшего 4

std::remove_if(vec.begin(), vec.end(), boost::bind(compare, _1, 4, true)); // удаление из последовательности всех элементов, больших 4

// и т. д.

В случае использования чистого STL для каждого случая пришлось бы искать подходящий функтор… Или писать его. Здесь необходимо обратить внимание на то, что в качестве первого параметра функции bind может выступать не только указатель на функцию, но и другой функциональный объект. Например:

std::find_if(vec.begin(), vec.end(), boost::bind(std::equal_to<int>(), _1, 4));

этот строчка найдет в массиве первый элемент, равный 4.

Использование с указателями на функции члены

Помимо указателей на глобальные функции и функторов, в качестве первого параметра можно указывать указатель на функцию-член какого-то класса. Предположим один из простейших вариантов использования:

class GraphicsObject

{
public:
   void Draw(Canvas* canvas, DrawModes mode);
};
// Объявляем коллекцию объектов этого класса:
std::list<GraphicsObject*> GraphObjects;

// А теперь нам нужно отрисовать все объекты коллекции на заданном канвасе
std::for_each(GraphObjects.begin(), GraphObjects.end(); ……..);

при использовании чистого STL нам бы пришлось писать свой собственный функтор, вызывающий у переданного объекта метод Draw с заданными параметрами. Но можно воспользоваться boost::bind:

std::for_each(GraphObjects.begin(), GraphObjects.end(), 
    boost::bind(&GraphObject::Draw, _1, canvas, mode));

в результате чего мы получаем требуемый результат. У всех объектов коллекции вызывается метод Draw, которому передаются параметры canvas и mode. При использовании указателя на функцию-член класса необходимо помнить о том, что первым параметром ей должен передаваться указатель на объект, для которого эта функция должна вызываться. В приведенном примере мы указываем, что метод должен вызываться у переданного в bind единственного параметра. Но никто не мешает нам в качестве первого передаваемого параметра указать this, или любой другой указатель. Но, как говориться в современных набивших оскомину рекламах, это еще не все.

Использование с указателем на член данных

Помимо указателей на члены-функции можно использовать указатели на члены данных. Синтаксис подобный и правила использования при этом почти такие же. Возьмем задачу. Есть структура и вектор ее экземпляров:

struct Point
{

    int x;
    int y;
};
 
std::vector<Point> PointsArray;

Теперь надо удалить из этого массива все элементы, у которых координата x равна нулю. Сделать это просто:

std::remove_if(PointsArray.begin(), PointsArray.end(), 
    boost::bind(std::equal_to<int>(), boost::bind(&Point::x, _1), 0));

Т. е. для каждого элемента массива вызывается вложенный связыватель, который возвращает значение соответствующего элемента структуры, после чего производится его сравнение с нулем. Тут мы видим еще одну возможность связывателей:

Каскадное использование связывателей

Связыватели могут вкладываться друг в друга. Один из вариантов такого вкладывания проиллюстрирован предыдущем примером. Единственное, что в этом случае надо «держать в голове» - это то, что плейсхолдеры, вне зависимости от того, в каком bind'ере (по уровню вложенности) они находятся, «адресуют» параметры самого внешго binder'а. Усложним предыдущий пример. Нужно выбросить все точки, имеющие нулевые координаты:

std::remove_if( PointsArray.begin(), PointsArray.end(), boost::bind(

    std::logical_and(), 
        boost::bind(std::equal_to<int>(), boost::bind(&Point::x, _1), 0),
        boost::bind(std::equal_to<int>(), boost::bind(&Point::y, _1), 0)

    )
));

Это хотя и работает так, как хочется, но выглядит слишком наворочено. По этому начиная с версии 1.33 в boost::bind появилась новая возможность -

Перегруженные операторы

Для упрощения приведенных выше многоэтажных конструкций для boost::bind (начиная с версии 1.33 boost'а) перегружены следующие операторы: !, ==, !=, <, ?, > и >=. Таким образом, приведенное выше выражение упрощается до:

std::remove_if( PointsArray.begin(), PointsArray.end(), boost::bind(

    boost::bind(&Point::x, _1) == 0 && boost::bind(&Point::y, _1) == 0)

    ));

Использование ссылок

Одна из неприятностей заключается в том, что если связываемая функция принимает какой-то из своих аргументов по ссылке, то с использованием boost::bind его (напрямую) передать нельзя. Т. е. например, в следующем случае:

void foo(int& a, int& b);

 
int a = 1, b = 2;
boost::bind(foo, a, b);

аргументы a и b по ссылке переданы не будут. Для того, чтобы действительно передать ссылки, необходимо делать такой вызов:

boost::bind(foo, boost::ref(a), boost::ref(b));

В этом случае foo, вызываемый из связывателя, будет действительно работать со ссылками на соответствующие переменные.

Полную документацию и примеры использования можно найти в оригинальной документации: http://www.boost.org/libs/bind/bind.html

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

#include <vector>
#include <boost/bind.hpp>
#include <boost/function.hpp>

class Test
{
public:
    void f_1()
    {
        std::cout << "void f()" << std::endl;
    }

    void f_2(int i)
    {
        std::cout << "void f_2(): " << i << std::endl;
    }

    void f_3(const int &i)
    {
        std::cout << "void f_2(): " << i << std::endl;
    }

    void two_params(int a, int b)
    {
        std::cout << "void two_params(int a, int b): " << a << ", " << b << std::endl;
    }
};

class Test2
{
public:
    void do_stuff(const std::vector<int>& v) 
    {
        std::copy(v.begin(), v.end(),  std::ostream_iterator<int>(std::cout, " "));
    }
};

void test(const std::string &a)
{
    std::cout << "void test(const std::string &a). a = " << a << std::endl;
}

void prn ( boost::function<void(int)> fn, int a )
{
    fn ( a );
}

int _tmain(int argc, _TCHAR* argv[])
{
    ////////////////////// bind //////////////////////
    std::cout << "boost::bind test" << std::endl;
    Test a;
    boost::bind(&Test::f_1, &a)();

    boost::bind(&Test::f_2, &a, 1)();
    int test_int(2);
    boost::bind(&Test::f_2, &a, _1)(test_int);

    boost::bind(&Test::f_3, &a, 3)();
    int test_int_2(4);
    boost::bind(&Test::f_3, &a, _1)(test_int_2);

    int one(100), two(200);
    boost::bind(&Test::two_params, &a, _1, _2)(one, two);
    boost::bind(&Test::two_params, &a, _2, _1)(one, two);

    std::string test_str("Hi there.");
    boost::bind(&test, test_str)();
    boost::bind(&test, _1)(test_str);
    std::cout << std::endl;
    
    ////////////////////// function //////////////////////
    std::cout << "boost::function test" << std::endl;
    
    boost::function<void (void)> func;
    func = boost::bind(&Test::f_1, &a);
    func();
    func = boost::bind(&Test::f_2, &a, 201);
    func();
    func = boost::bind(&Test::f_3, &a, 202);
    func();
    func = boost::bind(&Test::two_params, &a, 203, 204);
    func();

    boost::function<void (int)> func_2;
    func_2 = boost::bind(&Test::f_2, &a, _1);
    prn(func_2, 301);
    func_2 = boost::bind(&Test::f_3, &a, _1);
    prn(func_2, 302);

    int i_303(303), i_304(304), i_305(305), i_306(306);
    func_2 = boost::bind(&Test::two_params, &a, i_303, _1);
    prn(func_2, 1);
    func_2 = boost::bind(&Test::two_params, &a, _1, i_304);
    prn(func_2, 1);

    Test2 t;
    std::vector<int> vec;
    vec.resize(20);
    std::generate_n(vec.begin(), 20, rand);
    std::copy(vec.begin(), vec.end(),  std::ostream_iterator<int>(std::cout, " "));
    //simple_bind(&Test::do_stuff, t, _1)(vec);
    //boost::bind(&Test2::do_stuff, t, _1)(vec);

    return 0;
}

P.S.: Наглым образом спёрто здесь: [[doc:cpp:boost:bind]]

6 комментариев:

  1. не понятно, ты спёр или у тебя спёрли

    ОтветитьУдалить
  2. Буст бинд какаха - лямбда функции решают :)

    ОтветитьУдалить
  3. Анонимные функции с замыканиями и байндеры не взаимозаменяемые. Во-первых синтаксис байндера куда компактнее, во-вторых в анонимных функциях недопустим своеобразный полиморфизм относительно аргументов функции. Например я пишу: g = boost::bind(f, _1, _2); В таком случае я не знаю (не хочу либо не могу знать, что важно) какого типа аргументы у f. Допустим f объявлена как void f(int, int), в таком случае с анонимными функциями вышло бы так: g = [f](int a, int b){f(a,b);}. Казалось бы, все хорошо, в общем-то даже наглядно и можно смириться с тем, что сигнатура функции дублируется, но вот когда мы не знаем, какого типа аргументы у f - тогда и начинаются проблемы. Например такая ситуация возникает при написании шаблонных функций/классов - мы не можем объявить шаблонную анонимную функцию с набором неизвестных аргументов (слава Богу). Из этой ситуации есть выход, например в boost::type_traits я встречал инструменты для вывода типов аргументов функции из типа указателя на функцию, может быть они работают и с функторами вроде boost::function или std::function (да и вообще наверняка все это уже есть в std), но вот только такое решение правильным никто не назовет, тут байндер незаменим. К тому же, нельзя забывать, что в stl теперь есть и аналог boost::bind, очевидно его туда включили не случайно.

    ОтветитьУдалить
  4. C++ какаха - Haskell решает

    ОтветитьУдалить
  5. А статья и правда отличная, спасибо автору!

    ОтветитьУдалить