1. function包装器
1.1 遇到的问题
我们首先来看一行代码:
ret = func(x);
假设这行代码能够正常运行,那么这个func是什么呢?函数名?
函数指针?
函数对象?
lambda表达式对象?
很多种可能性,这些都是可调用的类型。这么多的类型可能会导致模板的效率低下。
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; } }; void Test1() { cout << useF(f, 11.11) << endl;//函数名 cout << useF(Functor(), 11.11) << endl;//函数对象 cout << useF([](double d)->double {return d / 4; }, 11.11) << endl;//lambda表达式 }
可以看到这里的useF函数模板被实例化了三份。但是使用function包装器就可以让只被实例化一份出来。
1.2 包装器的定义
function包装器,也叫做适配器。C++中的function本质是一个类模板。
模板参数说明:
Ret
:被包装的可调用对象的返回值类型Args…
:可调用对象的参数包
如何使用包装器呢?
int func(int a, int b) { return a + b; } struct Functor { public: int operator() (int a, int b) { return a + b; } }; class Plus { public: static int plusi(int a, int b) { return a + b; } int plusd(int a, int b) { return a + b; } }; void Test2() { //函数名(函数指针) function<int(int, int)> func1 = func; cout << func1(1, 2) << endl; //函数对象(仿函数) function<int(int, int)> func2 = Functor(); cout << func2(1, 2) << endl; //lambda表达式 function<int(int, int)> func3 = [](const int a, const int b)->int {return a + b; }; cout << func3(1, 2) << endl; //类的静态成员函数 function<int(int, int)> func4 = &Plus::plusi;//这里可以不加取地址符号 cout << func4(1, 2) << endl; //类的成员函数 function<int(Plus, int, int)> func5 = &Plus::plusd;//这里必须要加取地址符号,并且声明的时候需要显示的传类名 cout << func5(Plus(), 1, 2) << endl;//调用的时候需要传对象 }
1.3 解决问题
现在我们知道了包装器的用法了,那么怎么解决最开始的问题呢?
使用包装器包装可调用对象,然后将可调用对象传给useF
void Test3() { function<double(double)> func1 = f; function<double(double)> func2 = Functor1(); function<double(double)> func3 = [](double d)->double {return d / 4; }; cout << useF(func1, 11.11) << endl; cout << useF(func2, 11.11) << endl; cout << useF(func3, 11.11) << endl; }
可以看到,这里的useF函数模板只实例化出了一个对象。
1.4 包装器的其他应用
我们之前做过一道题:逆波兰表达式求解150. 逆波兰表达式求值 - 力扣(LeetCode),当时使用的方法代码如下
class Solution { public: int evalRPN(vector<string>& tokens) { stack<int> st; for(auto& str : tokens) { if(str == "+" || str == "-" || str == "*" || str == "/") { int right = st.top(); st.pop(); int left = st.top(); st.pop(); switch(str[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(str)); } } return st.top(); } };
在这里我们使用switch语句来判断运算类型,还需要在前面的条件里面枚举出运算类型,非常麻烦,如果在工程中,这段代码的可维护性就非常差,需要在if语句中增加枚举,在switch语句中增加case语句,这种情况就可以使用包装器来简化代码
- 首先建立运算符和执行函数之间的映射关系,当需要某一运算的时候就直接通过运算符找到对应的可调用对象调用即可。、
- 当运算类型增加时,只需要增加运算符和执行函数之间的映射关系即可
那么修改后的代码如下
class Solution { public: int evalRPN(vector<string>& tokens) { stack<int> st; map<string, function<int(int, int)>> opFuncMap = { {"+", [](int x, int y){return x + y;}}, {"-", [](int x, int y){return x - y;}}, {"*", [](int x, int y){return x * y;}}, {"/", [](int x, int y){return x / y;}} }; for(auto& str : tokens) { if(opFuncMap.count(str) == 0) { st.push(stoi(str)); } else { int right = st.top(); st.pop(); int left = st.top(); st.pop(); st.push(opFuncMap[str](left, right)); } } return st.top(); } };
2. bind
2.1 bind的定义
**bind时一个函数模板,就像是一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来”适应“原对象的参数列表。**一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用bind函数还可以实现参数顺序调整等操作。
参数列表说明:
Fn
:可调用对象Ret
:可调用对象的返回类型Args
:要绑定的参数列表:值或者时占位符
可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。调用bind的一般形式:auto newCallable = bind(callable,arg_list)
newCallable
:生成的新的可调用对象callable
:需要包装的可调用对象arg_list
:逗号分隔的参数列表,对应给定的callable的参数,当调用newCallable的时候,newCallable会调用callable,并传给他arg_list中的参数。
一般使用bind调整参数位置的时候,会使用一个类placeholders
,这是一个占位符类。
表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置,比如_1为newCallable的第一个参数,_2为第二个参数,以此类推。
此外,除了用auto接收包装后的可调用对象,也可以用function类型指明返回值和形参类型后接收包装后的可调用对象。
2.2 bind包装器绑定固定参数
首先我们来看一种无意义的绑定
int Sub(int a, int b) { return a - b; } void Test4() { function<int(int, int)> func1 = bind(Sub, placeholders::_1, placeholders::_2); cout << func1(1, 3) << " " << Sub(1, 3) << endl; }
绑定时第一个参数传入函数指针这个可调用对象,但后续传入的要绑定的参数列表依次是placeholders::_1和placeholders::_2,表示后续调用新生成的可调用对象时,传入的第一个参数传给placeholders::_1,传入的第二个参数传给placeholders::_2。此时绑定后生成的新的可调用对象的传参方式,和原来没有绑定的可调用对象是一样的,所以说这是一个无意义的绑定。
如果想让Sub的第二个参数固定绑定为10,就可以将绑定时的参数列表的palceholder::_2设置为10.
int Sub(int a, int b) { return a - b; } void Test4() { function<int(int)> func2 = bind(Sub, placeholders::_1, 10); cout << func2(2) << endl; }
此时调用绑定后新生成的可调用对象时就只需要传入一个参数,它会将该值减10后的结果进行返回。
2.3 bind包装器调整传参顺序
同样的对于上面的Sub函数的例子,我们想让传入的参数顺序进行调换,也可以使用bind实现。将placeholder::_1和placeholder::_2的位置进行调换即可。
void Test5() { function<int(int, int)> func = bind(Sub, placeholders::_2, placeholders::_1); cout << func(1, 2) << endl; }
根本原因就是因为,后续调用新生成的可调用对象时,传入的第一个参数会传给placeholders::_1,传入的第二个参数会传给placeholders::_2,因此可以在绑定时通过控制placeholders::_n的位置,来控制第n个参数的传递位置。
2.4 bind包装器的意义
- 将一个函数的某些参数绑定为固定的值,让我们在调用时可以不用传递某些参数。
- 可以对函数参数的顺序进行灵活调整。