std::bind(一):包装普通函数

前言

不知道大家在做项目写程序的过程中有没有遇到这样的情况,别的模块类提供了一个拥有很多参数接口函数,但是我这个功能只用到其中几个,其他的参数都是固定的,可是为了调用这个接口函数,不得不将所有的参数写一遍,每次写一堆固定参数都感觉在浪费生命。

有的人可能想到默认参数,的确,默认参数可以解决部分问题,因为默认参数只能出现参数列表的尾部,如果4个参数中,我需要传递的参数是第4个,而前3个参数想默认的话,默认参数是做不到这种效果的,并且别人的接口函数也不一定会有默认参数。

函数封装,这是一个办法,我们在自己的模块中添加一个对接口函数进行包装后的函数,将不变的参数进行固定,然后只留可变的参数供我们自己调用,如果我们有3种常用的调用方式可能就需要定义3个函数,这种方法可行,不过比较麻烦,而std::bind()函数就是为了包装函数而生的,使用起来更加方便。

std::bind()的作用

std::bind()的作用就是对原函数进行包装,可以将参数值固定,调换顺序然后生成新的函数供我们调用。举个例子,一块铁片你可以拿它来做很多事情,打造一下可以做成一把刀,敲敲打打可以做成一个桶,甚至直接拿来就可以挡窗户上的洞。std::bind()的作用就是把这块铁的作用固定,比如给她安上一个刀把,这样我们每次使用就可以把这块铁片当成菜刀来使用了。

std::bind()可以包装各种函数,但是这篇文章只总结一下包装普通函数的方法,因为在学习的过程中我发现单单是包装普通函数也会遇到很多问题,所以为了列举出诸多可能,说明各种注意事项,本文还是只关注于普通函数的包装,至于成员函数的包装还是放到以后的文章,给自己埋下一个坑。

在包装普通函数时,std::bind()的第1个参数就是原函数的名字,当然也可以是指向函数的指针,或者函数引用,从第2个参数开始,填写的内容依次对应原函数中的各个参数,所以说如果原函数是3个参数,如果想包装它,那么std::bind()需要传入4个参数,如果原函数是8个参数,那么包装它的std::bind()就需要传入9个参数,这里为了将原函数和包装后的函数参数建立联系,需要引入命名空间std::placeholders

placeholders的作用

std::placeholders的命名空间下有多个参数占位符,比如placeholders::_1placeholders::_2等等,最大为placeholders::_20,在包装普通函数时,固定的参数很好说,就是填写固定值就可以,但是要想原函数的参数和包装后函数的参数建立联系就需要用到刚刚提到的占位符, placeholders::_1就表示包装后函数的调用时的第1个参数,同理placeholders::_2就表示包装后函数的调用时的第2个参数。

有了占位符的概念,我们就可以推断出,包装后的函数与原函数相比,不但可以减少函数参数,也可以增加函数参数,虽然暂时没有想到什么实际的使用场景,但是理论上是可行的。

std::bind()使用测试

首先需要先引入头文件,免得找不到命名空间和函数定义

1
2
3
#include <iostream>
#include <functional>
using namespace std;

固定参数、调换顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void func1(int n1, int n2, int n3)
{
cout << n1 << ' ' << n2 << ' ' << n3 << endl;
}

void test1_1()
{
auto f1 = std::bind(func1, placeholders::_1, 101, placeholders::_2);
f1(11, 22); // same as call func1(11, 101, 22)
}

void test1_2()
{
auto f1 = std::bind(func1, placeholders::_2, 101, placeholders::_1);
f1(11, 22); // same as call func1(22, 101, 11)
}

// 输出
//11 101 22
//22 101 11

函数test1_1()展示了std::bind()函数最常见的用法,其中参数n2被固定为101,参数n1使用占位符placeholders::_1表示,表示包装后函数的第1个参数会传给形参n1使用,同理包装后函数的第2个参数会传给形参n3使用,所以调用函数f1(11, 22) 就等同于调用函数 func1(11, 101, 22)test1_2()函数简单展示了调换参数顺序的方法,只要明白了placeholders的作用,这两个例子也就明白了。

包装后函数的参数个数可增可减

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
void func2(int n1, int n2, int n3)
{
cout << n1 << ' ' << n2 << ' ' << n3 << endl;
}

void test2_1()
{
auto f2 = std::bind(func2, placeholders::_3, 101, placeholders::_1);
f2(11, 22, 33); // same as call func2(33, 101, 11)
}

void test2_2()
{
auto f2 = std::bind(func2, placeholders::_1, 101, placeholders::_1);
f2(11); // same as call func2(11, 101, 11)
}

void test2_3()
{
auto f2 = std::bind(func2, placeholders::_1, 101, placeholders::_2);
f2(11); // 报错,因为没有参数传给placeholders::_2
}

// 输出
//33 101 11
//11 101 11
//编译错误

其实在理解了placeholders的作用之后,这个测试结果也能想到的,函数test2_1()中使用了placeholders::_3,所以包装后函数的参数至少要传3个才不会报错,而test2_2()函数中使用了placeholders::_1,所以被包装函数调用时只需要传入一个参数,最后是函数test2_3(),绑定时引用了placeholders::_2,而在调用时只传了一个参数,所以出现编译错误。

bind()绑定时参数个数固定,类型需匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void func3(int n1, int n2, int n3)
{
cout << n1 << ' ' << n2 << ' ' << n3 << endl;
}

void test3_1()
{
auto f3 = std::bind(func3, placeholders::_1, 101);
//f3(11); // 编译错误,因为bind函数中少了一个参数
}

void test3_2()
{
auto f3 = std::bind(func3, placeholders::_1, 101, 102, 103);
//f3(11); // 编译错误,因为bind函数中多了一个参数
}

void test3_3()
{
auto f3 = std::bind(func3, placeholders::_1, "test", placeholders::_1);
//f3(11); // 编译错误,第二个参数类型不匹配,无法将参数 2 从“const char *”转换为“int”
}

看了之前的测试之后,是不是觉得参数的个数很随意,可以随便增加和减少,所以在绑定的时候也不好好写了,结果发现上述3个函数全部编译错误,test3_1()函数中因为绑定时少了一个参数而报错,test3_2()函数中因为绑定时多了一个参数而报错,而test3_3()函数中因为绑定时第二个参数的类型不匹配而报错,所以参数个数的增减只能是包装后的函数,而绑定时必须严格与原函数的参数个数以及类型相匹配。

普通函数的参数中有引用类型

弄明白上面的例子之后,可能会产生一种我会了的错觉,想象一下如果原函数参数中包含引用类型应该怎样写,可以自己先想一下,然后看看下面的例子

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
void func4(int n1, int n2, int& n3)
{
cout << n1 << ' ' << n2 << ' ' << n3 << endl;
n3 = 101;
}

void test4_1()
{
int n = 10;
auto f4 = std::bind(func4, 11, 22, n);
n = 33;
f4(); // same as call func4(11, 22, 10)
cout << "n = " << n << endl;
}

void test4_2()
{
const int n = 30;
auto f4 = std::bind(func4, 11, 22, n);
f4(); // same as call func4(11, 22, 30)
}

void test4_3()
{
int n = 30;
auto f4 = std::bind(func4, 11, 22, ref(n));
n = 33;
f4(); // same as call func4(11, 22, n)
cout << "n = " << n << endl;
}

void test4_4()
{
const int n = 30;
auto f4 = std::bind(func4, 11, 22, ref(n));
//f4(); // 编译错误,无法将参数 3 从“const int”转换为“int &”
}

// 输出
//11 22 10
//n = 33
//11 22 30
//11 22 33
//n = 101

如果能准确说出test4_1()函数的输出结果,那么后面的内容应该是不需要看了,如果只回答对了部分内容,或者干脆全错了,那么我们还有很长的路要走。

std::bind()的官方文档中有这样一句话,std::bind()函数中的参数在被复制或者移动时绝不会以引用的方式传递,除非你使用了std::ref()或者std::cref()包装的参数,如果知道了这个限定,就很容易明白函数test4_1()函数的输出结果了。

在函数test4_1()std::bind(func4, 11, 22, n)就相当于std::bind(func4, 11, 22, 10),所以输出结果为11 22 10,可是函数func4()中还有一句 n3 = 101;,这就很让人奇怪了,我们知道常数是没办法作为参数传递给可变引用变量的,如果说把10作为参数传递给参数int& n3肯定会报错,而函数test4_1()却正常执行,没有任何问题。

我们猜测常数10到参数int& n3并不是直接传递,而是发生了拷贝,而函数func4()中修改的n3变量也是修改的拷贝内容,所以我们做了test4_2()这个实验,发现将变量n改为常量也是可以正常执行的,甚至直接写成std::bind(func4, 11, 22, 10)也是没问题的,这也验证了我们上面的想法。

既然文档了提到了std::ref()std::cref()函数,那么我们想传递引用给原函数只能使用它们了,看下函数test4_3()的实现,这才是正确传递引用变量的方式,变量n被函数 std::ref() 包装之后,既能够感受到本函数中变量n的变化,也能够传入到原函数中被原函数的逻辑改变,并将结果反映回来。

函数test4_4()只是一个常量传递的简单测试,将一个常量作为可变引用变量来传递肯定是无法通过编译的,这在函数调用时很明确,但是在std::bind()加入之后显得有些混乱,只要记住一点,常量不应该被改变,如果传递之后内容可能会变化,那么很可能这种写法就是错误的。

总结

  1. 其实std::bind()函数测试到现在远远没有结束,配合std::ref()std::cref()函数会产生多种组合情况,不过主要的问题上面都提到了一些,出现问题的时候对照着定义和概念看看应该就能理解了。

  2. 需要理解std::placeholders的占位作用,它们是std::bind()函数最基本的用法。

完整代码

代码传送门

Albert Shi wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客