主要讲讲 std::bind, 也会说bind1st和bind2nd
以前常用的是 std::bind1st
和 std::bind2nd
函数适配器, 现在主推 std::bind
并且常常和 std::function
一起使用的就是 std::bind
, 用于绑定类成员函数.
(std::function 可以直接绑定全局函数, 静态函数; 但成员函数就要借助 bind )
引子
本文主要说说 std::bind1st 和 std::bind2nd 这类函数适配器, 以及其基本的实现方式.
其次讲解本文主推的 std::bind
, 但是注意, 编译标准: -std=c++11
.
正文
bind1st和bind2nd
绑定函数适配器, 将二元函数对象变成一元函数对象(也就是说, 原来二元函数对象中的一个参数是被绑定了的), 之后使用函数适配器即可, 使用起来比较简单.
bind1st绑定的是左边儿的参数(第一个参数), bind2nd是绑定的第二个参数(右边儿的参数).
直接看代码比较清楚:
1 |
|
注意编译的时候, 不要用 -std=c++11
这个标准, 会出现警告1
2
3
4
5
6
7
8
9
10
11
12
13
14
15warning: ‘template<class _Operation> class std::binder1st’
is deprecated [-Wdeprecated-declarations]
binder1st<plus<int>> plusObj = bind1st(plus<int>(), 1);
^~~~~~~~~
In file included from /usr/include/c++/6/bits/stl_function.h:1127:0,
from /usr/include/c++/6/string:48,
from /usr/include/c++/6/bits/locale_classes.h:40,
from /usr/include/c++/6/bits/ios_base.h:41,
from /usr/include/c++/6/ios:42,
from /usr/include/c++/6/ostream:38,
from /usr/include/c++/6/iostream:39,
from tmp.cpp:1:
/usr/include/c++/6/backward/binders.h:108:11: note: declared here
class binder1st
^~~~~~~~~
说明, bind1st和bind2nd 在新的标准中已经不提倡了. 应该用03的标准, 例如: g++ -g -Wall -std=c++03 tmp.cpp -o main
.
并且binder1st<plus<int>>
也会报错:1
error: ‘>>’ should be ‘> >’ within a nested template argument list
应该写成 binder1st<plus<int> >
.
实现bind1st
实现这样的一个 函数适配器, 形式上是在玩 函数模板 (调用函数模板my_bind1st返回一个函数对象my_binder1st), 实质上是把原来的二元操作, 转换为一元了.
首先需要一个类my_binder1st实现 operator()(param)
一元调用, 该类即作为函数对象类, 如下:
1 | template <class Op, Class Pa> |
那么此时, 至少需要两个成员变量, 保存 第一个参数
, 以及 原始二元函数对象
, 并且要在构造器里初始化(因为你可能需要该函数对象的实例, 而不仅仅是调用operator()(Param)), 代码就变成了这样:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21template <class Op, class Pa>
class my_binder1st
{
private:
Op binary_functor;
Pa first_param;
public:
my_binder1st(Op op, Pa pa)
{
binary_functor = op;
first_param = pa;
}
Pa operator() (Pa secondParam)
{
//返回调用 Op操作的结果
return binary_functor(first_param, secondParam);
}
};
然而根据std提供的方式, 我们还不应该直接调用其构造函数, 得到my_binder1st的对象, 应该有相关的模板方法(或者使工厂方法), 就简单些一个函数模板吧:
1 | template<class Op, class Pa> |
之后直接使用my_bind1st调用, 或者去得到my_binder1st对象, 再进行调用, 都可以.
完整的代码可以是, 如下: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
using namespace std;
template <class Op, class Pa>
class my_binder1st
{
private:
Op binary_functor;
Pa first_param;
public:
my_binder1st(Op op, Pa pa)
{
binary_functor = op;
first_param = pa;
}
Pa operator() (Pa secondParam)
{
return binary_functor(first_param, secondParam);
}
};
template<class Op, class Pa>
my_binder1st<Op, Pa> my_bind1st(Op functor, Pa first)
{
return my_binder1st<Op, Pa>(functor, first);
}
int main(void)
{
my_binder1st<plus<int>, int> plusObj = my_bind1st(plus<int>(), 1);
cout << plusObj(2) <<endl; //1+2=3
return 0;
}
//编译 g++ -g -Wall -std=c++03 tmp.cpp -o main
其实还是蛮简单的, 实际上我们总是在用适配之后的函数对象 my_binder1st, 所以, 就看你怎么去包装了(其实还可以写的更好, 关键在模板类的封装上).
std::bind
bind1st 和 bind2nd 不提倡了, 现在推荐使用的是 std::bind
, 不仅仅包含了原来的两个功能, 还提供了全局性的绑定, 具体可以查看 cppreference.com , 我下面就详细说说看.
毫不夸张的说, 现在的各种绑定的, 都可以直接使用 std::bind, 无论是指针, 参数, 函数, 包括lambda表达式 等等, 当然可能和 std::function
结合的比较紧(std::function绑定成员函数不借助std::bind也是可以完成的,只需要传一个 *this
变量进去就好了).
例如你要绑定一个二元函数的参数, 可以这么做:1
auto fun = bind(&func, std::placeholders::_2, std::placeholders::_1);
调用的时候通过 fun(1,2) 实现调用 func(2,1) , 其中func可以是指针, 函数对象, 函数, 包括lambda表达式 等等. 这种调用的时候使用占位符的叫做延迟计算绑定( 后绑定
), 而调用 bind 时直接传入参数的叫做 预先绑定
, 例如:1
auto fun = bind(&func, xxxx, yyy);
区别:
- bind预先绑定的参数需要传具体的变量或值进去, 对于预先绑定的参数, 是pass-by-value的
- 对于不事先绑定的参数,需要传 std::placeholders 进去, 从 _1 开始, 依次递增, placeholder是pass-by-reference的
(记得占位符绑定的时候传参是引用即可; 或者你预先绑定的时候就传入引用参数)
注意: 对于绑定的指针&引用类型的 “参数” , 使用者需要保证在可调用实体调用之前, 这些指针所指是可用的(绑定本身不对安全性做担保).
占位符的讲解
std::placeholders是一个占位符. 当使用 bind 生成一个 新的可调用对象
时, std::placeholders表示新的可调用对象的参数位置.1
bind(&func, std::placeholders::_2, std::placeholders::_1)
解释:
- 你调用 新的函数对象fun 时第2个参数(即占位符_2代表的参数)和原来函数对象 func 的第1个参数匹配, 而fun的第1个占位符参数和原来函数对象func的第2个参数匹配. 也就是placeholder是代表你使用新的函数对象的顺序.
- 该语句中传入参数是给原来的func用的, 所以是按照顺序传参的, 即新的函数对象被调用时传入的参数
_2
是给原来函数对象的第1个参数用的(因为语句中是写在前面的). 也就是你在bind语句的书写顺序就是原函数的参数顺序; 参数的绑定可以由你自己指定, 但是建议按照顺序来, 以免出错.
总结: 你就记得你总是在用绑定后生成的新对象在进行调用, 那么新对象的参数位置是用 placeholders 进行标记的, 例如 fun(2,1);
这里实参2
就是第一个placeholder, 实参1
是第二个placeholder; 然后根据你定义的bind规则填充原函数func(placeholder::_2, placeholder::_1);//这样进行调用
std::bind 绑定的参数的个数不受限制, 绑定的具体哪些参数也不受限制, 由用户指定, 这个bind才是真正意义上的绑定.
绑定类型
普通函数(直接在bind语句写函数的名字,但是为了避免重载函数的干扰, 最好转换成函数指针, 用函数指针进行)
见下面代码:1
2
3
4
5
6
7
8
9//有两个重载函数参数不同, 但是名字都叫做f
typedef int (*f_int_int)(int, int);
typedef int (*f_double_double)(double, double);
f_int_int pf1 = f;
f_double_double pf2= f;
cout << bind(pf1, 1, 1) <<endl; //相当于直接&f
cout << bidn(pf2, 1.0, 1.0) << endl;函数对象(直接在bind语句写函数对象的实例)
例如:1
2
3std::bind(std::greater<int>(), _1, 10);
//这样就生成了一个funtion对象, 可以直接在此基础上进行实例调用
std::bind(std::greater<int>(), _1, 10)(11);(11<10, 该表达式是false)稍微注意一下, 标准库中的函数对象都是有定义
result_type
的, 如果你自定义的函数对象, 那么绑定的时候要定义一下.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class add : public std::binary_function<int, int, void> //这里void就定义了result_type是void
{
public:
//或者遵循规范, 内部定义result_type
typedef void result_type;
void operator()(int i, int j) const
{
std::cout << i + j << std::endl;
}
};
int main(void)
{
std:: vector<int> v;
v.push_back(1);
v.push_back(3);
std::for_each(v.begin(), v.end(), std::bind(add,10,_1);
//相当于
//std::for_each(v.begin(), v.end(), std::bind<void>(add,10,_1);
}即
bind<result_type>(functor, ...);
- 绑定成员函数或者对象
一定要传入this指针, 即对象的地址, 下面给一个简单的案例: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
struct Foo {
void print_sum(int n1, int n2)
{
std::cout << n1+n2 << '\n';
}
int data = 10;
};
int main()
{
using namespace std::placeholders; // for _1, _2, _3...
// bind to a pointer to member function
Foo foo;
auto f3 = std::bind(&Foo::print_sum, &foo, 95, _1);
f3(5);//5作为foo.print_sum的第二个参数
// bind to a pointer to data member
//绑定的时候, 没有传入this指针, 则调用时要传入(指针或者引用)
auto f4 = std::bind(&Foo::data, _1);
std::cout << f4(foo) << '\n';//实际上传入的是引用
//上面f4还可以写成如下形式(绑定时就传入this指针)
auto f5 = std::bind(&Foo::data, &foo); //这里传入引用也是可以的
std::cout << f5() << std::endl;
//当然也可以拿着去绑定pair对象的first和second成员
//pair<int, string> p(1, "1");
//cout << bind(&pair<int, string>::fist, p)() << endl;
}
而且用于STL算法时, std::bind 降低了算法函数绑定对象的要求:
1 | vector<point> v(10); |
更加复杂的, 嵌套式绑定, 运算符重载式绑定, 最好不要用, 绑定标准C库的函数等, 太过复杂, 容易出错.(或者选用lambda表达式)
(注意: 绑定标准C库函数的时候, 可能还会扯上调用方式 _stdcall
, _fastcall
, 以及修饰 extern "C"
, 具体可以参考相关的宏控制)
补充:
- bind的返回值是可调用实体, 可以直接赋给std::function对象;
- 山寨一个bind
- 绑定虚函数的时候和绑定成员函数没有区别, 但是虚函数的行为还是根据调用时的实例确定
- 绑定成员函数还可以直接使用 std::mem_fn
- 如果你还是不能理解绑定过程, 那么我推荐你看一篇 图 该图大致画出了相关意思, 并且给出了bind的大致实现思路.
尾巴
在使用 std::bind
的时候注意一下, 绑定的参数的安全性, 顺序, 以及绑定成员成分(函数, 数据)时, 一定要在后面传入this指针或者本对象的地址, 如果你不绑定, 那么实际调用的时候就要传入.
绑定实际上是, 统一了多种调用形式, 并且用 function 对象接收, 从而实现完全的面向对象操作方法.
参考资料
- http://blog.think-async.com/2010/04/bind-illustrated.html
(图解了std::bind的绑定和延迟计算过程)