c++11 std::bind 详解

1. 概述

std::bind 函数定义在头文件中,是一个函数模板,它就像一个函数适配器,接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。

2. 函数原型

std::bind函数有两种函数原型,定义如下:

1
2
template< class F, class... Args > bind( F&& f, Args&&... args ); 
template< class R, class F, class... Args > bind( F&& f, Args&&... args );

std::bind返回一个基于f的函数对象(obj),调用obj时参数被绑定到args上。f的参数要么被绑定到具体的值,要么被绑定到 placeholders(占位符,如_1, _2, …, _n).

其中占位符_1,_2表示,当执行obj函数时的第1,第2 … 第n个参数。

3. 参数及用法解释

3.1 参数

f:一个可调用对象(可以是函数对象、函数指针、函数引用、成员函数指针、数据成员指针),它的参数将被绑定到args上。
args:绑定参数列表,参数会被值或占位符替换,其长度必须与f接收的参数个数一致。

1
2
3
4
5
调用可调用对象时,绑定参数被std::move,调用参数被std::forward,你得根据可调用对象的行为来判断std::bind返回的函数对象是否可以多次调用。

绑定参数可以是bind表达式,占位符被替换为外层的调用参数,相当于用调用参数来调用这个bind表达式,求值后用来调用外层bind表达式——我是在读源码读到一半一脸懵逼的时候才知道这件事的。这与可调用对象被std::bind以后可以再std::bind并不冲突,因为bind表达式一个是作为绑定参数,另一个是作为可调用对象。

std::bind有个重载,可以用模板参数指定bind表达式的operator()的返回类型。

3.2 调用形式

调用std::bind的一般形式为:

1
auto newCallable = std::bind(callable, arg_list);

其中,newCallable 本身是一个可调用对象,arg_list 是一个逗号分隔的参数列表,对应给定的 callable 的参数。即,当我们调用 newCallable 时,newCallable 会调用callable,并传递给它 arg_list 中的参数。

3.3 返回类型

std::bind 的返回类型是一个函数对象,这个函数对象是一个未指定类型T的函数对象,这个函数对象的 std::is_bind_expression::value == true;这个函数对象包含以下成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1. 对象成员
一个由std::forward<F>(f)构造而来的std::decay<F>::type类型的对象,一个对象的每一个参数类型都是由std::forward<Arg_i>(arg_i)构造而来的std::decay<Arg_i>::type。简单来说,std::decay<F>::type对象保存了调用std::bind时传递过来的f参数,而若干个std::decay<Arg_i>::type则保存了传递过来的args参数(一个std::decay<Arg_i>::type保存一个args)。
2. 构造函数
如果T的所有对象成员都是可拷贝的,则它自身也是可拷贝的;如果它的所有对象成员都是可移动构造的,则它自身也是可移动构造的。
3. 成员类型 result_type(从C++17开始result_type已经被弃用)
·如果F是函数指针或者成员函数指针,result_type就是F的返回值类型
·如果F是一个拥有(或者说定义了)result_type的类类型,那么T的result_type就是F::result_type,即使result_type已经在T中被定义过
4. 成员函数 operator()
这是最应该了解的,因为在实际使用过程中,我们调用std::bind得到的返回值就是用来作为函数调用的。
bind的返回值T,假设我们这样调用:g(a1, a2, a3, … ai); 此时g内部保存的std::decay<F>::type类型的对象将被调用, 它将会按照如下的方式来为a1, a2, …, ai 绑定值。
· 如果调用bind时指定的是reference_wrapper<T>类型的,比如在调用bind时使用了std::ref 或者 std::cref来包装args,那么调用g内部的这个对象时,对应参数会以T&类型传入std::decay<F>::type类型的对象.
· 如果在创建g时,使用了嵌套的bind,即g = bind(fn, args…)的参数列表args中,存在某个arg:使得std::is_bind_expression<decltype(arg)>::value == true, 那么这个嵌套的bind表达式会被立即调用,其返回值会被传给ret里的_MyFun作为参数(也就是说嵌套的bind返回值会被当做ret调用时的参数), 如果嵌套的bind里用到了占位符placeholder, 这些placeholder将会从ret的调用参数ret(a1, a2, … ai)中对应位置选择.
· 如果在创建g时,使用了占位符placeholders, 即 g = bind(fn, arg1, arg2, …, _1, _2, …), (对于_1, _2…, 有std::is_placeholder<T>::value != 0). 那么a1, a2, …, ai会以转发的形式forward<ai>(ai)传递给_MyFun, a1对应_1, a2对应_2, 以此类推.
否则,ret内部保存的args,即上文提到的_Mybargs(bind调用时绑定的参数们)将被以左值的形式传给_MyFun以完成调用,这些参数和g有相同cv限定属性.
如果g(a1, a2, …, ai)中,有哪些ai没有匹配任何的placeholders,比如在调用bind时,placeholder只有_1, 而g(a1, a2, a3), 那么a2, a3就是没有匹配的,没有被匹配的参数将被求值,但是会被丢弃。
如果g被指定为volatile(volatile or const volatile),结果是未定义的。

4 类成员函数回调

在c++中,常用的回调函数场景是,在一个类A中,有一个普通成员函数a,在类B中,有一个普通成员函数b,在b中,想要回调函数a,这才是c++回调函数的正确打开方式。

先上一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>

#include <functional>

using namespace std;
using namespace std::placeholders;

typedef std::function<void(int,int)> Fun;

class B{
public:
void call(int a,Fun f)
{
f(a,2);
}
};

class Test{
public:
void callback(int a,int b)
{
cout<<a<<"+"<<b<<"="<<a+b<<endl;
}

void bind()
{
Fun fun=std::bind(&Test::callback,this,_1,_2);
B b;
b.call(1,fun);
}

};
int main()
{
Test test;
test.bind();
return 0;
}

上面的程序中,Test类中的bind函数调用B类中的call函数,b中的call函数又反过来回调Test类中的callback函数。记住function和bind都是c++11标准函数,编译的时候要加-std=c++11。

在分析上面程序之前,先介绍一下两个辅助函数,分别是bind函数和function函数,这两个函数之前是boost函数成员,现在加入到c++11标准中,使用更加方便。

bind函数

定义在头文件functional中。可以看成是对一个函数的改造器,可以借助于集合的观点来说(尽管可能没这回事),可以将bind函数看作是返回一个子函数。这个子函数可以是bind绑定的函数的子集,也可以是本身。

一般常用语法是: newFunName=bind(oldFunName,arg_list);

bind函数返回一个新的函数对象。其中bind第一个参数是oldFunName,它是待绑定的函数名,arg_list是oldFunName的参数列表。注意,这个参数列表是旧函数的参数列表,前面提到,返回的是子函数。我们可以随便给子函数定几个参数,但是肯定不能多于bind所绑定的原函数的参数个数。举个例子:

1
2
3
4
//g是一个有两个参数的可调用对象
auto g=bind(f,a,b,_2,c,_1);
//其中f是具有5个参数的函数
//当我们调用g(x,y)时,实际调用的是f(a,b,y,c,x)

在这个示例中,我们可能要调用f函数,并传入5个参数,但是我们现在调用g(x,y),只要传入两个参数,同样能达到这个效果。当然,我费这么多事其实肯定不是为了省几个参数,主要还是为了将一个函数转化成一个可以作为回调的函数指针,可以看成是原函数指针的别名。

上面出现的_1,_2是它的占位符,bind最多可以使用9个占位符。这个占位符命名在std的placeholders中,使用时,要使用using std::placeholders.

function函数

function是一个函数对象的容器

如function<int(int,int)> fun; fun是一个函数模板,可以接受两个int型参数,并返回一个int型参数。平时可以将它赋值给一个函数指针。

例如上面的回调函数: Fun fun=std::bind(&Test::callback,this,_1,_2);

其中bind用于绑定一个Test类的callback函数,它有两个参数,在这里,因为它是一个类成员函数,中间传入一个this指针,另外两个_1和_2则是它的两个参数。bind返回一个函数指针,将它赋给fun,fun作为一个函数容器,容纳bind函数返回的临时函数指针。 这样就成功的将fun作为一个函数参数的别名,可以用于传给回调函数了。

关于bind和function函数,再举一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>  
#include <functional>
using namespace std;

typedef std::function<void ()> fp;
void g_fun()
{
cout<<"g_fun()"<<endl;
}
class A
{
public:
static void A_fun_static()
{
cout<<"A_fun_static()"<<endl;
}
void A_fun()
{
cout<<"A_fun()"<<endl;
}
void A_fun_int(int i)
{
cout<<"A_fun_int() "<<i<<endl;
}

//非静态类成员,因为含有this指针,所以需要使用bind
void init()
{
fp fp1=std::bind(&A::A_fun,this);
fp1();
}

void init2()
{
typedef std::function<void (int)> fpi;
//对于参数要使用占位符 std::placeholders::_1
fpi f=std::bind(&A::A_fun_int,this,std::placeholders::_1);
f(5);
}
};
int main()
{
//绑定到全局函数
fp f2=fp(&g_fun);
f2();

//绑定到类静态成员函数
fp f1=fp(&A::A_fun_static);
f1();

A().init();
A().init2();
return 0;
}

5 注意

如可调用 (Callable) 中描述,调用指向非静态成员函数指针或指向非静态数据成员指针时,首参数必须是引用或指针(可以包含智能指针,如 std::shared_ptr 与 std::unique_ptr),指向将访问其成员的对象。

所以在调用类的成员函数的时候首参必须是this

参考

1
2
3
4
5
https://blog.csdn.net/afei__/article/details/81985937
https://blog.csdn.net/hyp1977/article/details/51784520
https://en.cppreference.com/w/cpp/utility/functional/bind
https://www.cnblogs.com/jerry-fuyi/p/12633621.html
https://blog.csdn.net/xiexievv/article/details/50517964