文章目录
- 一. 为什么要有包装器?
- 二. 什么是包装器?
- 三. 包装器的使用
- 四. bind 函数模板
- 1. 为什么要有 bind ?
- 2. 什么是 bind ?
- 3. bind 的使用场景
一. 为什么要有包装器?
function 包装器,也叫作适配器。C++ 中的 function 本质上是一个类模板,也是一个包装器。接下来我们来看看,C++ 为什么引入 function 呢?
在 C++ 中,可调用对象有以下三种:函数名/函数指针、仿函数对象、lambda 表达式
// 加法普通函数
int NormalPlus(int num1, int num2)
{
return num1 + num2;
}
// 加法仿函数
class FunctorPlus
{
public:
int operator()(int num1, int num2)
{
return num1 + num2;
}
};
int main()
{
// 加法 lambda 表达式
auto LambdaPlus = [](int num1, int num2) {return num1 + num2; };
// 1、调用普通函数
NormalPlus(10, 20);
// 2、调用仿函数对象
FunctorPlus obj;
obj(10, 20);
// 3、调用 lambda 表达式
LambdaPlus(10, 20);
}
可以看到,它们的调用方式可以不能说相似,只能说是一模一样,且它们函数体中执行的内容都是完全相同的;但是它们彼此之间的类型不同,不能进行相互赋值等操作。
上面是三种不同的可调用对象,那么它们的类型不同还可以理解;但是在 lambda 表达式中,就算功能相似的 lambda 表达式,它们直接的类型也互不相同:
这就导致在很多应用场景中,lambda 表达式使用起来是非常不便的。比如之前我们大量进行回调函数时,会采用函数指针的方式,构建一个函数指针数组,调用时按其对应的下标调用即可。但是 lambda 表达式则不然,每一个 lambda 表达式的类型不相同,我们没办法去开辟一个定类型的数组,也就意味着传统的函数指针的方式是不可行的。
包装器的诞生就是为了解决这个问题,通过包装器,可以让功能相似的可调用对象(函数名/函数指针、仿函数对象和 lambda 表达式)的类型统一,使其可以相互联系,相互转化。
二. 什么是包装器?
包装器是个类模板,它的定义在头文件 functional 中
下面看看具体的示例:
也就是说,在 function 的模板列表中,第一个参数是返回值的类型,随后在括号里依次传入形参的类型;而后在赋值的时候,只需要让其等于已经定义过的可调用对象,这样就算包装完成了,最后我们可以使用这个包装好的对象来代替之前的可调用对象,去执行它们的功能。
三. 包装器的使用
还是刚刚的例子,我们分别包函数名/函数指针,仿函数对象,lambda 表达式,看看包装完之后它们各自的类型是什么
// 加法普通函数
int NormalPlus(int num1, int num2)
{
return num1 + num2;
}
// 加法仿函数
class FunctorPlus
{
public:
int operator()(int num1, int num2)
{
return num1 + num2;
}
};
int main()
{
// 加法 lambda 表达式
auto LambdaPlus = [](int num1, int num2)->int{return num1 + num2; };
// 使用包装器包装函数名
function<int(int, int)> function1 = NormalPlus;
// 使用包装器包装函数地址
function<int(int, int)> function2 = &NormalPlus;
// 使用包装器包装仿函数对象(这里的 FunctorPlus() 是一个匿名的仿函数对象)
function<int(int, int)> function3 = FunctorPlus();
// 使用包装器包装 lambda 表达式
function<int(int, int)> function4 = LambdaPlus;
// 其类型都为 class std::function<int __cdecl(int,int)>
cout << typeid(function1).name() << endl;
cout << typeid(function2).name() << endl;
cout << typeid(function3).name() << endl;
cout << typeid(function4).name() << endl;
// 它们之间也可以任意赋值
function1 = function3;
function3 = function4;
return 0;
}
------运行结果-------
class std::function<int __cdecl(int,int)>
class std::function<int __cdecl(int,int)>
class std::function<int __cdecl(int,int)>
class std::function<int __cdecl(int,int)>
可以发现,这几个可调用对象无论最开始它们是什么类型,最后都被包装器给包装成了 function<int(int, int)> 的类型。既然类型相同了,它们之间也就可以进行:互相赋值、共同存储在一个数组中的操作了。
通过包装器,我们可以轻轻松松实现函数回调:
int main()
{
//实现一个计算器
map<string, function<int(int, int)>> calculator =
{
{"加法",[](int a,int b) {return a + b; }},
{"减法",[](int a,int b) {return a - b; }},
{"乘法",[](int a,int b) {return a * b; }},
{"除法",[](int a,int b) {return a / b; }}
};
cout << calculator["加法"](10, 20) << endl;
cout << calculator["减法"](10, 20) << endl;
}
------运行结果-------
30
-10
如果没有包装器,那么只能采用普通函数构建函数指针数组,这既会产生大量命名冲突的风险,又会导致程序的简洁性大大降低。这里如果使用包装器去包装的话,便把 lambda 表达式简洁易读的特点放到了最大。
四. bind 函数模板
1. 为什么要有 bind ?
前面一直说的是普通函数,别忘了还有类中的成员函数,那包装器如何包装和调用成员函数呢?
class A
{
public:
// 普通成员函数
int Plus(int num1, int num2)
{
return num1 + num2;
}
// 静态成员函数
static int PlusStatic(int num1, int num2)
{
return num1 + num2;
}
};
int main()
{
// 静态成员函数(没有this指针,其实和普通函数没什么区别,函数名和函数地址相同)
function<int(int, int)> funcStatic1 = A::PlusStatic;
function<int(int, int)> funcStatic2 = &A::PlusStatic;
// 非静态成员函数(有this指针,this其实就是该类的对象,位于在参数列表中第一个参数位置)
// 包装非静态成员函数时,不能用函数名,必须使用函数地址;对非静态成员函数而言,函数名和函数地址不同)
function<int(A, int, int)> func1 = &A::Plus;
// 调用包装器对象
cout << funcStatic1(10, 20) << endl;
cout << funcStatic2(10, 20) << endl;
cout << func1(A(), 10, 20) << endl; // 非静态成员函数需要传入一个对象,然后通过这个对象去调用
return 0;
}
------运行结果-------
30
30
30
总结一下关于成员函数的包装:
- 类的静态成员函数和普通函数性质一样,它们的函数名和函数地址等价;但是非静态成员函数的地址则必须使用取地址运算符“&”。
- 包装非静态成员函数时需要增加一个参数(this 指针,this 其实就是一个实例化的类对象),因为非静态成员函数需要用对象去调用,且非静态成员函数的第一个参数是隐藏 this 指针,因此在包装时需要指明第一个形参的类型为类的名称。
- 静态成员函数因为没有 this 指针,所以它本质上其实和普通函数一样。
- 调用包装好的非静态成员函数时,注意在参数列表中第一个参数位置传入该类的一个实例化对象,通常都是传的都是匿名对象。
为了让包装好的非静态成员函数使用起来更简单(不想每次使用时都要传入一个实例化对象), C++11 增加了新特性:bind 模板
2. 什么是 bind ?
std::bind 函数定义在头文件 functional 中,是一个函数模板,它也有点像上面的包装器(适配器),接受一个可调用对象(函数/函数名、仿函数对象、lambda 表达式),然后生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收 N 个参数的可调用对象 Func,通过绑定一些参数,返回一个接收 M 个(通常 M <= N)参数的新函数。另外,使用 std::bind 模板还可以修改参数的传参顺序。
具体说的话,bind 可以去给可调用对象(通常是静态成员函数)参数列表中的参数指定缺省值,或者更改形参的接收顺序,然后生成一个新的可调用对象来“适应”原对象的参数列表。
下面是 bind 的定义:
具体示例:
在 bind 的第一个参数中,我们输入被绑定的可调用对象的名称,后面再依次输入传参进来的参数的顺序。
3. bind 的使用场景
在绑定时,参数列表中参数的个数和顺序我们可以进行一些小调整:
作用一:给参数设定缺省值
作用二:调整传参顺序
int normal_plus(int num1, int num2)
{
return num1 + num2;
}
int main()
{
// 交换参数的顺序
function<int(int, int)> fplus = bind
(
normal_plus,
placeholders::_2,//第一个参数传入 num2
placeholders::_1 //第二个参数传入 num1
);
// 传入参数顺序为绑定的顺序
fplus(10, 3); //3+10;
}