一、 function包装器
function包装器也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。
那么function包装器究竟是为了解决什么问题呢??C++11设置出来的初衷是什么呢??
1.1 解决回调问题
历史回溯:
1、在C++还没有出现的时候(C语言时期),我们去调用函数是通过函数名去调用的,但是在有些时候我们对函数调用的一个需求并不只是这么简单的场景。比如说在某些场景下会需要进行函数回调(发生某个动作再去调用某个函数——>可以理解为一个指令对应调用一个函数),比方说我们的cmd,cmd指令所产生的行为从底层来说就是通过指令去回调函数,而相关的函数可能会被存储在一个函数指针数组里面,然后再去和指令做一个相关的映射处理。但是在C语言中的函数指针和指针数组的设计以及对应类型的书写方式都是比较复杂的,可读性差也不好理解。
2、而为了解决这个问题,C++引入了仿函数(STL六大组件之一)帮助我们解决了这个问题,从底层来说创建了一个相关的类,然后通过在类中重载括号,然后用一个对应的匿名类对象就可以像函数那样去使用!但是仿函数太“重”了,也就是说哪怕一个很简单的函数我也要去外域单独写一个类出来,可能会有两个比较麻烦的地方:(1)比如一个结构体有这样一个需求,就是里面的任何一个变量都根据情况去升序和降序,那每一个变量我们都需要去写一个升序的仿函数和降序的仿函数,然后再把对应的仿函数传给sort(),这样代码会非常冗余。(2)如果这是一个大型的工程,分了很多个文件,而前期编写程序的人不注重命名的风格,导致我们在看到这个仿函数的时候不知道他代表什么含义,那么就需要去外域找源码,就会浪费很多时间,不适合后期别人的维护。
3、lambda表达式最早是C#出现的,而C++11的时候发现其可以很好地帮助我们解决这个问题,于是抄了作业,引入了lambda表达式,其底层其实也是仿函数,只不过进行了相关的封装,其相比较于一般的仿函数最大的特点就是可以在函数的内域去书写函数的实现,然后再通过匿名对象去调用,这样哪怕命名风格不好,程序员也能快速在函数中找到相关的代码,便于后期的维护,并且对于一些短小的函数,lambda表达式真的是屡试不爽,非常好用,而且他和普通函数相比,除了传参以外他还可以捕获内域的变量!!lambda表达式的引入可以说是C++11的一个重大发明。
4、从函数指针->仿函数->lambda表达式,C++无不在为了函数更为方便调用而去努力改变,lambda表达式看似已经是最优解了,其实并不然,lambda表达式本质上来说就是创建了一个可以像函数一样去使用的匿名对象,但是他不太适合解决函数回调的问题,比如说我们想用unordered_map去存一个kv模型,k代表指令,而v代表调用的函数,我们会发现如果用lambda的话,他并没有明确的类型,所以我们无法把这个类型传进unordered_map中。
5、为了解决这个问题,C++11引入了function包装器,他可以将lambda表达式的类型(函数指针和仿函数也可以)进行包装,包装之后就可以实现函数回调了!!
std::function在头文件<functional>
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参
比如下面我们用一个unordered_map来帮助我们实现回调。
void swap_func(int& r1, int& r2)//函数指针
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
struct Swap//仿函数
{
void operator()(int& r1, int& r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
};
int main()
{
int x = 0, y = 1;
cout << x << " " << y << endl<<endl;
auto swaplambda = [](int& r1, int& r2) {//lambda表达式
int tmp = r1;
r1 = r2;
r2 = tmp;
};
function<void(int&, int&)> f1 = swap_func;//可以包装函数指针、仿函数、lambda 相当于是可以通过function可以给他们的类型起个别的名字,然后可以像普通函数一样去调用
f1(x, y);
cout << x << " " << y << endl << endl;
function<void(int&, int&)> f2 = Swap();//仿函数和lambda都是传对象, 函数指针传函数名字
f2(x, y);
cout << x << " " << y << endl << endl;
function<void(int&, int&)> f3 = swaplambda;
f3(x, y);
cout << x << " " << y << endl << endl;
//回调的类似用法
unordered_map < string, function<void(int&, int&) >> cmdOP = { {"函数指针", swap_func},{"仿函数",Swap()} ,{"lambda",swaplambda} };
//包装的前提, 参数类型和参数的个数必须一样
cmdOP["函数指针"](x, y); //回调 发生某个动作再去调用某个函数。 ——>实现指令对应函数(cmd指令)
cout << x << " " << y << endl << endl;
cmdOP["仿函数"](x, y);
cout << x << " " << y << endl << endl;
cmdOP["lambda"](x, y);
cout << x << " " << y << endl << endl;
return 0;
}
1.2 一统函数类型的天下
总结一下:
(1)函数指针:书写反人类,不易理解。
(2)仿函数:太重了,不太好找源码
(3)lambda表达式:没有明确的类型,不适合函数回调的。
他们的作用都差不多,但并不是一类人,function包装器让他们化敌为友。成为一类人。
我们来看看下面这个程序:
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 函数名
cout << useF(f, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl;
// lamber表达式
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
return 0;
}
ret = func(x); 上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!
通过上面的程序验证,我们会发现useF函数模板实例化了三份。但其实他们的功能本质上来说都是一样的,却实例化出了三份,而function包装器很好地解决了这些问题!!
#include <functional>
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 函数名
function<double(double)> func1 = f;
cout << useF(func1, 11.11) << endl;
// 函数对象
function<double(double)> func2 = Functor();
cout << useF(func2, 11.11) << endl;
// lamber表达式
function<double(double)> func3 = [](double d)->double { return d / 4; };
cout << useF(func3, 11.11) << endl;
return 0;
}
总结一下就是function的出现解决了以下问题:
1、比如有将可调用对象放进容器里的需求(回调) 把他们的类型造出来
2、做类型统一的验证,防止实例化出多份对象
1.3 用包装器解决OJ题
在没有学过包装器之前,我们可以这样去解决。
class Solution {
public:
int evalRPN(vector<string>& tokens)
{
stack<int> st;//栈帮助我们计算
for(auto&s:tokens) //遍历字符串
{
if(s=="+"||s=="-"||s=="*"||s=="/")//如果是操作符的话,取栈顶两个元素计算
{
//先取右操作数,再取左操作数
int right=st.top(); st.pop();
int left=st.top();st.pop();
//因为可能会有不同的情况,所以要用一个swich去计算
switch(s[0])
{
case '+':
st.push(left+right);
break;
case '-':
st.push(left-right);
break;
case '*':
st.push(left*right);
break;
case '/':
st.push(left/right);
break;
}
}
else st.push(stoi(s));如果不是操作符的话,利用stoi将字符串转化成数字并入栈
}
return st.top();
}
};
学会了包装器以及lambda表达式后,可以让代码更加精炼。
class Solution {
public:
int evalRPN(vector<string>& tokens)
{
stack<int> st;//栈帮助我们计算
unordered_map<string,function<int(int,int)>> cmpOP=
{
{"+",[](int i,int j){return i+j;}},
{"-",[](int i,int j){return i-j;}},
{"*",[](int i,int j){return i*j;}},
{"/",[](int i,int j){return i/j;}}
};
for(auto&str:tokens) //遍历字符串
{
if(cmpOP.count(str))//如果是操作符的话,取栈顶两个元素计算
{
//先取右操作数,再取左操作数
int right=st.top(); st.pop();
int left=st.top();st.pop();
st.push(cmpOP[str](left, right));
}
else st.push(stoi(str));如果不是操作符的话,利用stoi将字符串转化成数字并入栈
}
return st.top();
}
};
1.4 成员函数的包装
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
1、成员函数需要取地址,比较特殊,要加一个类域和& 静态成员函数可以不加(因为没有this指针) 但是建议加上。
function<int(int, int)> f1 = &Plus::plusi;
cout << f1(1, 2) << endl;
2、对于非静态成员的成员函数,由于其有this指针,而非静态成员函数本质上是通过this指针去调用的,所以在这个地方必须把this指针传进去。传Plus*相当于是 用他来调用相关的函数
function<double(Plus*, double, double)> f2 = &Plus::plusd;
Plus ps;
cout << f2(&ps, 1.1, 2.2) << endl;
3、也可以不一定要用Plus*套一层指针,去传地址,也可以直接传匿名对象给Plus
function<double(Plus, double, double)> f3 = &Plus::plusd;
cout << f3(Plus(), 1.11, 2.22) << endl;//这边其实经过了特殊处理 传的对象就是用这个对象去调用这个函数(可以理解为这边为了解决成员函数的包装而规定的)
使用匿名对象的错误方法:(以下方法都不可以)
(1)用const引用去接受匿名对象——>类型不匹配(直接按规则理解)
(2)给匿名对象取地址——>因为匿名对象是右值
根据上面的分析进行总结:
(1)包装成员函数必须加类域,非静态成员还得加&,非静态成员不需要。
(2)想要包装非静态成员函数必须要有相关的对象或者指针去调用才可以,并且类型必须匹配。比如说Plus* 接受&plus ,而Plus接受Plus()。 一般只有这两种
二、bind绑定器
2.1 bind绑定器的作用
为什么要引入bind绑定器呢??
我们发现function包装非静态成员函数的时候,每次都要传一个对象过去,而且是固定死的,每次只要调用都得传一次特别麻烦,所以bind绑定器就是为了解决这类问题。
std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。
// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
我们直接利用bind来解决之前function包装非静态成员函数需要传对象的问题
function<double(double, double)> f4 = bind(&Plus::plusd, Plus(), placeholders::_1, placeholders::_2);//将必须传的参数给写死,这样调用的时候可以简化调用
//比如说在有些场景下,某个函数的参数可能有7、8个 但是其中3/4个是我们的默认权限所以可以不用传的(就是传死的),只有后面几个参数会影响函数运行的结果,这个时候我们用bind将一部分参数写死,这样可以简化调用的接口
// function是一个类模版 bind(绑定)是一个函数模版 调整可调用对象的参数 (可以调整顺序也可以调整个数)
cout << f4(1.11, 2.22) << endl;
2.1.1 调整参数顺序
通过placeholder来调整参数的位置,_x表示的是占位符
2.1.2 调整参数的个数
这里调整参数的个数意思是将一部分参数给写死。
int main()
{
function<int(int, int)> f1 = Sub;
cout << f1(10, 5) << endl;
// 调整参数顺序
function<int(int, int)> f2 = bind(Sub, placeholders::_2, placeholders::_1);
//cout << typeid(f2).name() << endl;
cout << f2(10, 5) << endl;
auto f3 = bind(Sub, placeholders::_2, placeholders::_1);
//cout << typeid(f3).name() << endl;//这个说明bind看似没有返回值,实际上底层封装得很深,要用auto去推断才能推断出来
// 调整参数个数,有些参数可以bind时写死
function<int(int)> f4 = bind(Sub, 20, placeholders::_1);//_1和_2相当于是占位符
cout << f4(5) << endl;
return 0;
}
2.2 bind绑定器的常见场景
调整参数的顺序相对来说还是不多见的,最常用的应该就是调整参数的个数。
比如说前面的非静态成员函数的包装,每次都要传一个对象过来,我们通过bind把他给写死,这样后面调用的时候就可以更加方便了。
比如说某些函数的参数有7、8个,但是实际上其中有些参数是可以使用一些默认系统已有的东西,这个时候如果我们不去更改底层的东西,就没有必要去动那一部分参数,因此我们就可以通过bind将这部分参数写死,这样调用会更为简单。
2.3 bind绑定器的思考
1、bind绑定器真的没有返回值吗??
我们可以用auto去推断一下
这说明bind是有返回值的,只不过封装得特别深。
2、bind绑定器可以把中间的参数写死吗??
是可以的,bind的第一个参数是对应的函数,后面的参数和函数的参数是一一对应的,除了写死的部分,其他部分借由placeholders::_x来调整顺序