我们首先来看一行代码:
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包装器就可以让只被实例化一份出来。
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;//调用的时候需要传对象
}
现在我们知道了包装器的用法了,那么怎么解决最开始的问题呢?
使用包装器包装可调用对象,然后将可调用对象传给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函数模板只实例化出了一个对象。
我们之前做过一道题:逆波兰表达式求解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();
}
};
**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类型指明返回值和形参类型后接收包装后的可调用对象。
首先我们来看一种无意义的绑定
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后的结果进行返回。
同样的对于上面的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个参数的传递位置。
本节完…