技术: C++11 Bind 探究

主要讲讲 std::bind, 也会说bind1st和bind2nd

以前常用的是 std::bind1ststd::bind2nd 函数适配器, 现在主推 std::bind
并且常常和 std::function 一起使用的就是 std::bind , 用于绑定类成员函数.
(std::function 可以直接绑定全局函数, 静态函数; 但成员函数就要借助 bind )

引子

本文主要说说 std::bind1st 和 std::bind2nd 这类函数适配器, 以及其基本的实现方式.

其次讲解本文主推的 std::bind , 但是注意, 编译标准: -std=c++11 .

正文

bind1st和bind2nd

绑定函数适配器, 将二元函数对象变成一元函数对象(也就是说, 原来二元函数对象中的一个参数是被绑定了的), 之后使用函数适配器即可, 使用起来比较简单.
bind1st绑定的是左边儿的参数(第一个参数), bind2nd是绑定的第二个参数(右边儿的参数).

直接看代码比较清楚:

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
#include <iostream>
#include <algorithm>
#include <functional>
#include <vector>

using namespace std;


int main(void)
{
binder1st<plus<int> > plusObj = bind1st(plus<int>(), 1);

cout << plusObj(2) << endl; //3

cout << "---------------" << endl;

vector<int> v;
for (int i = 0; i<10; i++) {
v.push_back(i+1);
}

//less or equal than 4
int n = count_if(v.begin(), v.end(), bind2nd(less_equal<int>(),4));
cout << "less or equal than 4: " << n << endl;


return 0;
}

注意编译的时候, 不要用 -std=c++11 这个标准, 会出现警告

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
warning: ‘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
2
3
4
5
6
7
8
9
10
template <class Op, Class Pa>
class my_binder1st
{
public:
//外部调用者提供 secondParam
Pa operator() (Pa secondParam)
{
//返回调用 Op操作的结果
}
};

那么此时, 至少需要两个成员变量, 保存 第一个参数, 以及 原始二元函数对象 , 并且要在构造器里初始化(因为你可能需要该函数对象的实例, 而不仅仅是调用operator()(Param)), 代码就变成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)
{
//返回调用 Op操作的结果
return binary_functor(first_param, secondParam);

}
};

然而根据std提供的方式, 我们还不应该直接调用其构造函数, 得到my_binder1st的对象, 应该有相关的模板方法(或者使工厂方法), 就简单些一个函数模板吧:

1
2
3
4
5
template<class Op, class Pa>
my_binder1st<Op, Pa> my_bind1st(Op functor, Pa first)
{
return my_binder1st<Op, Pa>(functor, first);
} //注意返回值是值传递

之后直接使用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
#include <iostream>
#include <algorithm>
#include <functional>


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
    3
    std::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
    22
    class 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
    #include <random>
    #include <iostream>
    #include <memory>
    #include <functional>
    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
2
3
4
5
6
7
8
9
10
vector<point> v(10);
vector<x> v2(10); //专门记录point的横坐标

//初始化 容器v
...

transform(v.begin(), v.end(), v2.begin(), bind(&point::x, _1));
for(auto x: v2){
cout << x << ",";
}

更加复杂的, 嵌套式绑定, 运算符重载式绑定, 最好不要用, 绑定标准C库的函数等, 太过复杂, 容易出错.(或者选用lambda表达式)

(注意: 绑定标准C库函数的时候, 可能还会扯上调用方式 _stdcall, _fastcall, 以及修饰 extern "C", 具体可以参考相关的宏控制)

补充:

  • bind的返回值是可调用实体, 可以直接赋给std::function对象;
  • 山寨一个bind
  • 绑定虚函数的时候和绑定成员函数没有区别, 但是虚函数的行为还是根据调用时的实例确定
  • 绑定成员函数还可以直接使用 std::mem_fn
  • 如果你还是不能理解绑定过程, 那么我推荐你看一篇 该图大致画出了相关意思, 并且给出了bind的大致实现思路.

尾巴

在使用 std::bind 的时候注意一下, 绑定的参数的安全性, 顺序, 以及绑定成员成分(函数, 数据)时, 一定要在后面传入this指针或者本对象的地址, 如果你不绑定, 那么实际调用的时候就要传入.

绑定实际上是, 统一了多种调用形式, 并且用 function 对象接收, 从而实现完全的面向对象操作方法.

参考资料

  1. http://blog.think-async.com/2010/04/bind-illustrated.html
    (图解了std::bind的绑定和延迟计算过程)
文章目录
  1. 1. 引子
  2. 2. 正文
    1. 2.1. bind1st和bind2nd
    2. 2.2. 实现bind1st
    3. 2.3. std::bind
  3. 3. 尾巴
  4. 4. 参考资料
|