我正在参加「掘金·启航计划」
1.function包装器概念
function包装器也叫做适配器,C++11中的function本质是一个类模板,也是一个包装器。
其实这些都是可调用对象
:C语言的函数指针、仿函数/函数对象、lambda表达式、今天说的是包装器,主要是function包装器与bind包装器。
类模板原型:
template <class T> function;
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:Ret
:被包装的可调用对象的返回值类型。Args...
:被包装的可调用对象的形参类型。
废话不多说,我们先来简单地使用一下是把,分别包装函数指针、仿函数、Lambda表达式、成员函数:
包装函数指针、仿函数、Lambda表达式:
#include <functional>
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator()(int a, int b)
{
return a + b;
}
};
int main()
{
//1.包装函数指针
function<int(int, int)> f1;
f1 = f;
cout << f1(10, 20) << endl;
function<int(int, int)> f2(f);
cout << f2(10, 20) << endl;
//2.包装仿函数
/*Functor f1();
function<int(int, int)> f3(f1);*/
function<int(int, int)> f3 = Functor();
cout << f3(10, 20) << endl;
//3.包装Lambda表达式
function<int(int, int)> f4 = [](const int a, const int b) {return a + b; };
cout << f4(10, 20) << endl;
return 0;
}
注意头文件#include
包装成员函数:(成员函数分为静态和非静态,本质还是函数指针)
class P
{
public:
static int P1(int a, int b)
{
return a + b;
}
int P2(int a, int d)
{
return a + d;
}
};
int main()
{
//静态成员函数
function<int(int, int)> f5 = &P::P1;//成员函数要加上类域限制
cout << f5(10, 20) << endl;
//非静态成员函数,this指针不允许显式传递
function<int(P, int, int)> f6 = &P::P2;
cout << f6(P(), 10, 20) << endl;
}
取静态成员函数的地址可以不用
"&"
,但是取非静态成员函数的地址就必须带上"&"
包装非静态成员函数是需要注意:非静态成员函数的第一个参数是隐藏this指针,所以在包装的时候需要指明第一个形参的类型为类的类型
包装器本质就是对各种可调用对象进行类型的统一。
2.function包装器统一类型
我们提供一个函数模板useF:
传入该函数模板的第一个参数可以是任意的
可调用对象
:如我们上面所说的函数指针、仿函数、lambda表达式等。useF中定义了静态变量count,每次调用时将count的值和地址进行了打印,可判断多次调用时调用的是否是同一个useF函数。
函数模板useF代码:
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);
}
现在我们传入第二个参数相同的类型,但是传入的可调用对象的类型是不同的,那么在编译阶段该函数模板就会被实例化多次:
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
double f(double i)
{
return i / 2;
}
int main()
{
//函数名
cout << useF(f, 9.9) << endl;
//函数对象
cout << useF(Functor(), 9.9) << endl;
//lambda表达式
cout << useF([](double d)->double {return d / 3; },9.9) << endl;
return 0;
}
由于函数指针、仿函数、lambda表达式是不同的类型,那么函数模板useF也会实例化出三份,结果打印出来也是不同的。
但是如果有需求:不实例化出三份,因为虽然三次调用传入可调用对象类型不同,但是调用对象的返回值与形参类型相同
此时我们就可以使用包装器对这三个不同的调用对象进行包装了,分别包装这三个可调用对象来调用useF函数,这时候就值实例化出一份useF函数。
#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);
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
double f(double i)
{
return i / 2;
}
int main()
{
//函数名
function<double(double)> f1 = f;
cout << useF(f1, 9.9) << endl;
//函数对象
function<double(double)> f2 = Functor();
cout << useF(f2, 9.9) << endl;
//lambda表达式
function<double(double)> f3 = [](double d)->double {return d / 4; };
cout << useF(f3,9.9) << endl;
return 0;
}
这时三次调用useF函数所打印count的地址就是相同的,并且count在三次调用后会被累加到3,表示这一个useF函数被调用了三次。
3.function包装器优化代码
还记得我们写过的题目:逆波兰表达式求值吗?
解题过程:
定义一个栈,遍历字符串,如果是数字直接入栈(字符串转化成数字stoi),如果遍历到的字符是加减乘除运算符,则从栈顶取出两个数字,进行相关的运算,最后在将结果入栈。遍历完字符串之后,栈顶的数字就是最终的计算结果
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
for(int i = 0;i<tokens.size();i++)
{
if(tokens[i]=="+"||tokens[i]=="-"||tokens[i]=="*"||tokens[i]=="/")
{
long long num1 = st.top();
st.pop();
long long num2 = st.top();
st.pop();
if(tokens[i]=="+") st.push(num2+num1);
if(tokens[i]=="-") st.push(num2-num1);
if(tokens[i]=="*") st.push(num2*num1);
if(tokens[i]=="/") st.push(num2/num1);
}
else
{
st.push(stoi(tokens[i]));
}
}
return st.top();
}
};
对于上面的代码,通过多个if语句判断进行哪一个运算,如果运行增加,那么我们就需要继续添加if判断语句了。
这时候,我们可以利用function包装器来优化上面的代码:
建立各个运算符与其对应需要执行的函数间的映射关系,当执行某一个运算时就可以直接通过运算符找到对应的函数执行;
当运算符类型增加时,我们只需要建立新增运算符与其对应函数间的映射关系即可。
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
//命令与动作的映射
map<string,function<int(int,int)>> opFuncMap =
{
{"+",[](int x,int y)->int{return x+y;}},
{"-",[](int x,int y)->int{return x-y;}},
{"*",[](int x,int y)->int{return x*y;}},
{"/",[](int x,int y)->int{return x/y;}},
};
for(auto&e:tokens)
{
//操作数进栈
if(opFuncMap.count(e)==0)
{
st.push(stoi(e));
}
//操作符
else
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
st.push(opFuncMap[e](left,right));
}
}
return st.top();
}
};
function包装器可以将可调用对象的类型进行统一,便于我们对其进行统一化;包装后明确8可调用对象的返回值和形参类型,更加方便使用者使用。
bind包装器
1.bind包装器概念
bind函数定义在头文件中,也是一个函数模板,就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表。
bind函数模板原型:
template <class Fn, class... Args>
/* unspecified */ bind(Fn&& fn, Args&&... args);
template <class Ret, class Fn, class... Args>
/* unspecified */ bind(Fn&& fn, Args&&... args);
参数说明:fn:可调用的对象,...args:要绑定的参数列表。
2.bind包装器绑定固定参数
bind绑定可以与function结合,placeholders:命名空间,_1,_2:占位对象
- 无意义绑定
int Plus(int a, int b)
{
return a + b;
}
int main()
{
//表示绑定函数plus参数分别由调用func1的第一,二个参数指定
std::function<int(int, int)> func1 = std::bind(Plus, placeholders::_1, placeholders::_2);
cout << func1(10, 20) << endl;
return 0;
}
第一个参数传入函数指针,后面传入绑定的参数列表依次是placeholders::_1和placeholders::_2,表示后续调用新生成的可调用对象时,第一个参数传给placeholders::_1,第二个参数传给placeholders::_2。此时绑定后生成的新的可调用对象的传参方式,和原来没有绑定的可调用对象是一样的,所以说这是一个无意义的绑定。
- 固定绑定参数
参数是可以进行固定绑定的:比如我们把Plus函数的第二个参数固定绑定为10,可以在绑定时将参数列表的placeholders::_2设置为20:
#include <functional>
int Plus(int a, int b)
{
return a + b;
}
int main()
{
function<int(int)> func = bind(Plus, placeholders::_1,20);
cout << func(2) << endl;//22
return 0;
}
3.bind包装器调整传参顺序
我们以Sub类为例子,对于下面代码中的Sub类的成员函数sub,第一个参数是this指针,所以如果想要调用sub时不用对象进行调用,那么我们就可以通过上面所说的固定绑定参数,把sub成员函数的第一个参数固定绑定为Sub对象:
class Sub
{
public:
int sub(int a, int b)
{
return a - b;
}
};
int main()
{
function<int(Sub, int, int)> func4 = &Sub::sub;
cout << func4(Sub(), 10, 20) << endl;//-10
//绑定固定参数,不需要传Sub()
function<int(int, int)> func5 = bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);
cout << func5(10,20) << endl;//-10
return 0;
}
此时调用绑定后生成的可调用对象时,就只需要传入用于相减的两个参数了,因为在调用时会固定帮我们传入一个匿名对象给this指针.
bind包装器的传参是可以进行调整的,将sub成员函数用于相减的两个参数的顺序交换,那么直接在绑定时将placeholders::_1和placeholders::_2的位置交换一下就行了:
class Sub
{
public:
int sub(int a, int b)
{
return a - b;
}
};
int main()
{
function<int(int, int)> func5 = bind(&Sub::sub, Sub(), placeholders::_2, placeholders::_1);
cout << func5(10,20) << endl;//10
return 0;
}
bind包装器可以将一个函数的某些参数绑定为固定值,让我们在调用时可以不传递某些参数,也可以对函数参数的顺序进行调整