Здравствуйте, недавно начал изучать Boost.Asio, и в одном из примеров не могу понять,
что делает функция bind() в коде данной программы.
Если кто знает, для чего придназначена эта функция, пожалуйста, объясните. Так как
я недавно начал углубляться в библиотеку STL и Boost. Заранее спасибо.
io_service service;
size_t read_complete(char * buff, const error_code & err, size_t bytes)
{
if ( err) return 0;
bool found = std::find(buff, buff + bytes, '\n') < buff + bytes;
// we read one-by-one until we get to enter, no buffering
return found ? 0 : 1;
}
void handle_connections()
{
ip::tcp::acceptor acceptor(service, ip::tcp::endpoint(ip::tcp::v4(),8001));
char buff[1024];
while ( true)
{
ip::tcp::socket sock(service);
acceptor.accept(sock);
int bytes = read(sock, buffer(buff), boost::bind(read_complete,buff,_1,_2));
std::string msg(buff, bytes);
sock.write_some(buffer(msg));
sock.close();
}
}
int main(int argc, char* argv[])
{
handle_connections();
}
Ответы
Ответ 1
std:bind (я буду говорить об std::bind, так как эта функция перенесена в стандарт
C++ из boost) - это адаптер функциональных объектов, который позволяет адаптировать
функциональные объекты под заданное число параметров.
Чтобы было более ясно ее назначение, допустим вы решили написать функцию вывода произвольного
целочисленного массива в поток std::cout . Ваша программа могла бы выглядеть следующим
образом:
#include
void display( const int a[], size_t n )
{
for ( size_t i = 0; i < n; i++ )
{
std::cout << a[i] << ' ';
}
std::cout << std::endl;
}
int main()
{
const size_t N = 10;
int a[N] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
display( a, N );
}
Вывод программы на консоль:
1 2 3 4 5 6 7 8 9 10
Но затем вам пришла в голову идея усовершенствовать функцию вывода таким образом,
чтобы она не просто выводила элементы массива, как они есть, но и делала какие-то с
ними преобразования перед выводом.
Поэтому вы решили добавить параметр в функцию, который будет представлять некоторую
операцию над элементами массива. Этот третий параметр вы объявили как функцию с одним
параметром. Используя эту функцию, вы можете, например, вывести на консоль удвоенные
значения элементов массива с помощью лямбда-выражения:
#include
void display( const int a[], size_t n, int operation( int ) )
{
for ( size_t i = 0; i < n; i++ )
{
std::cout << operation( a[i] ) << ' ';
}
std::cout << std::endl;
}
int main()
{
const size_t N = 10;
int a[N] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
auto doubled = []( int x ) { return 2 * x; };
display( a, N, doubled );
}
Вывод на консоль будет
2 4 6 8 10 12 14 16 18 20
В данном случае возможно использовать лямбда-выражение doubled, потому что оно может
быть неявно преобразовано в функцию с одним параметром, так как не имеет замыкания.
Но, допустим, вы решили выводить на печать не только удвоенные значения элементов
массива, но и значения элементов, умноженных на некоторый коэффициент, который задается
во время выполнения программы.
В этом случае ваша программа могла бы выглядеть следующим образом
#include
void display( const int a[], size_t n, int operation( int ) )
{
for ( size_t i = 0; i < n; i++ )
{
std::cout << operation( a[i] ) << ' ';
}
std::cout << std::endl;
}
int main()
{
const size_t N = 10;
int a[N] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int factor = 2;
auto multiplies = [&factor]( int x ) { return factor * x; };
display( a, N, multiplies );
factor = 3;
display( a, N, multiplies );
}
Однако она не будет компилироваться. Проблема заключается в том, что если лямбда-выражение
содержит замыкание,
auto multiplies = [&factor]( int x ) { return factor * x; };
^^^^^^^^^
то оно не имеет неявного преобразования в указатель на функцию.
Поэтому придется написать шаблонную функцию, которая будет иметь шаблонный параметр
операции и в качестве аргумента может принимать не только функции, но и объекты классов,
который имеют оператор вызова функции operator ().
Ваша программа будет выглядеть следующим образом:
#include
template
void display( const int a[], size_t n, Operation operation )
{
for ( size_t i = 0; i < n; i++ )
{
std::cout << operation( a[i] ) << ' ';
}
std::cout << std::endl;
}
int main()
{
const size_t N = 10;
int a[N] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int factor = 2;
auto multiplies = [&factor]( int x ) { return factor * x; };
display( a, N, multiplies );
factor = 3;
display( a, N, multiplies );
}
Вывод на консоль соответственно будет:
2 4 6 8 10 12 14 16 18 20
3 6 9 12 15 18 21 24 27 30
Но теперь, допустим, вы решили вывести на печать квадраты значений элементов массива.
Вы опять бы могли использовать лямбда-выражение, как, например,
auto_square = []( int x ){ return x * x; };
И ваша программа успешно бы выполнилась, используя это лямбда-выражение.
Но вы знаете, что стандарт уже имеет функциональный объект std::multiplies, который
осуществляет операцию x * y для двух аргументов своего оператора вызова функции operator ().
Как им воспользоваться? Оператор этого функционального объекта принимает два аргумента,
тогда как в нашей функции display функциональный объект принимает только один аргумент.
Для этого используются функциональные адаптеры и, в частности, std::bind. Он может
"превратить" функциональный объект std::multiplies из объекта, который принимает два
аргумента в объект, который принимает один аргумент.
Программа будет выглядеть следующим образом:
#include
#include
template
void display( const int a[], size_t n, Operation operation )
{
for ( size_t i = 0; i < n; i++ )
{
std::cout << operation( a[i] ) << ' ';
}
std::cout << std::endl;
}
int main()
{
const size_t N = 10;
int a[N] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
auto square = bind( std::multiplies(), std::placeholders::_1, std::placeholders::_1 );
display( a, N, square );
std::cout << std::endl;
}
Вывод на консоль будет
1 4 9 16 25 36 49 64 81 100
Внутри функции display объект square вызывается как square( a[i] ),
который в свою очередь делегирует работу объекту класса std::multiplies,
вызывая его как
std::multiplies()( a[i], a[i] );
Конечно конструктор std::multiplies не вызывается, так как объект данного класса
уже был создан, когда создавался объект square
auto square = bind( std::multiplies(), std::placeholders::_1, std::placeholders::_1 );
Вызывался только operator () этого объекта.
В вашем примере вызов read требует в качестве третьего аргумента функциональный объект,
который принимает только два аргумента:
int bytes = read(sock, buffer(buff), boost::bind(read_complete,buff,_1,_2));
Однако, вы хотите, чтобы вызывалась функция read_complete, которая принимает три
аргумента вместо двух. В этом случае используется адаптер bind, который сам получает
два аргумента, как того требует вызов функции read, но делегирует работу функции read_complete,
передавая ей три аргумента: два своих, обозначенных как _1 и _2, и которые ей передает
внутри своего тела функция read, когда ее вызывает, и один дополнительный, buff, который
она сохранила внутри себя, когда использовалась в качестве аргумента вызова read.
Ответ 2
bind это немного интересная функция (не путать с функцией работы сокетами, которая
также называется bind!!!). Ее задача создать одну функцию на базе другой, подставив
часть аргументов (это называется "частичным применением").
Посмотрим глубже. Функция read требует трех параметров - сокета, буфера для данных
и "функции завершения" (вообще то у этой бустовой read всего восемь различных перегруженных
вариантов). Эта функция будет вызываться, что бы определить, что данных прочитано достаточно.
Там же в справке приведена сигнатура этой функции. У нее два параметра: код ошибки
последнего чтения и сколько байт прочитано. Но что делать, если нет подходящей функции?
Для этого есть bind. Он первым аргументом принимает функцию (в данном случае read_complete,
а дальше аргументы. Так как исходная функция read_complete имела три аргумента, а функции
read нужно только два, то один аргумент подставляется явно (первый), а вместо двух
других - заглушки специального вида.
В результате, когда функции read нужно будет вызвать функцию завершения, она вызовет
read_complete, первый аргумент ей подставит bind, а два вторых будут подставлены с
аргументов.
Если попробовать написать код схематически, то все выглядит где то так
io_service service;
size_t read_complete(char * buff, const error_code & err, size_t bytes)
{
if ( err) return 0;
bool found = std::find(buff, buff + bytes, '\n') < buff + bytes;
// we read one-by-one until we get to enter, no buffering
return found ? 0 : 1;
}
char buff[1024];
size_t new_func(const error_code & err, size_t bytes)
{
return read_complete(buff, err, bytes);
}
void handle_connections()
{
ip::tcp::acceptor acceptor(service, ip::tcp::endpoint(ip::tcp::v4(),8001));
while ( true)
{
ip::tcp::socket sock(service);
acceptor.accept(sock);
int bytes = read(sock, buffer(buff), new_func);
std::string msg(buff, bytes);
sock.write_some(buffer(msg));
sock.close();
}
}
Как видите, мне пришлось сделать буфер глобальным, что бы новоиспеченная функция
имела к нему доступ. На самом деле переменная глобальной не становится, но это был
самый простой способ эмулировать поведение.
Эта функция bind пришла в с++ с функциональных языков вида haskell и лисп, где подобное
встречается на каждом шагу.