function包装器
function包装器介绍
function是一种函数包装器 也叫做适配器 它可以对可调用对象进行包装 C++中的function本质就是一个类模板
它的类模板原型如下
template <class T> function; // undefined template <class Ret, class... Args> class function<Ret(Args...)>;
模板参数说明:
- Ret:被包装的可调用对象的返回值类型
- Args…:被包装的可调用对象的形参类型
下面我们会使用包装器对可调用对象进行包装
首先我们写出下面的代码做好前置准备
int f(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; } double plusd(double a, double b) { return a + b; } };
包装函数指针(函数名)
// 包装函数指针(函数名) function<int(int, int)> f1 = f; cout << f1(3, 3) << endl;
包装仿函数(函数对象)
// 包装仿函数(函数对象) function<int(int, int)> f2 = Functor(); cout << f2(3, 3) << endl;
包装lambda表达式
// 包装lambda表达式 function<int(int, int)> f3 = [](int x, int y)mutable->int {return x + y; }; cout << f3(3, 4);
类的静态成员变量
// 类的静态成员函数 function<int(int, int)> f4 = &Plus::plusi; // 对于静态成员变量来说 &符号可以省略 cout << f4(3, 4);
类的非静态成员变量
// 类的非静态成员变量 function<int(Plus, int, int)> f5 = &Plus::plusd; cout << f5(Plus(), 2, 1);
注意事项:
- 包装时要指明返回值还有各种参数类型 然后将可调用对象赋值给function包装器即可 包装后的的function对象就可以像普通函数一样调用了
- 对于静态成员函数 & 可加可不加 对于非静态成员函数 &必须要加
- 包装非静态成员函数时候第一个参数是隐藏的this指针 所以说在包装时要指明第一个是类类型的参数
function包装器统一类型
对于以下函数模板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); }
- 传入该模板的第一个参数可以是任意类型的可调用对象 比如说匿名函数 函数指针 函数对象等
- 我们在该模板的内部设置了一个静态成员变量count 并且使用两个count语句打印出count的大小和位置 这两个语句的意义是判断是否公用一个实例
我们保持第二个参数相同 试着传入不同的第一个参数
// 函数指针 useF(f, 3); // 函数对象/仿函数 useF(Functor(), 3); // 匿名函数/lambda表达式 useF([](int x) {return x / 2; }, 3);
这里说明下
- 在参数中写lambda表达式时 最后的分号不写 用逗号代替标识lambda表达式的结束
最后的运行结果是这样子的
通过观察我们可以发现两点
- count的地址都不相同 所以说函数模板实例化出了三个不同的对象
- count的大小都是1 说明了三个实例化的函数都只调用了一次
一个模板 为了完成一个相同的任务竟然被实例化出了三份 这是追求高效的C++所不能忍受的 所以我们可以使用C++11中的包装器来解决这个问题
function<double(double)> f1 = f; // 函数 function<double(double)> f2 = Functor(); // 函数 function<double(double)> f3 = [](double x)mutable->double {return x / 2; }; // lamada表达式 useF(f1, 3); useF(f2, 3); useF(f3, 3);
我们分别对于三个类型的函数对象进行包装之后再使用usef调用之
结果上
- 我们可以发现三次打印出来的count都是同一地址
- count的大小为3 说明这个实例化的函数被条用了三次
原理上
- 造成这样现象的本质是因为经过function的包装之后 它们的类型都变成了function类型
以下是证明代码和截图
function<double(double)> f1 = f; // 函数 function<double(double)> f2 = Functor(); // 函数 function<double(double)> f3 = [](double x)mutable->double {return x / 2; }; // lamada表达式 cout << typeid(f1).name() << endl; cout << typeid(f2).name() << endl; cout << typeid(f3).name() << endl;
function简化代码
我们再求解逆波兰表达式这道题目的时候是这么操作的
- 定义一个栈 并且遍历整个字符串
- 如果遍历到的是字符则直接入栈
- 如果遍历到的是运算符则从栈中取出两个数字运算 运算完毕后入栈
- 重复上面的步骤直到字符串遍历完毕 最后栈顶元素就是结果
我之前写的leetcode代码如下
class Solution { public: int evalRPN(vector<string>& tokens) { stack<int> st; for (auto x : tokens) { if (x == "+" || x =="-" || x =="*" || x =="/") { long num1 = st.top(); st.pop(); long num2 = st.top(); st.pop(); if (x == "+") st.push(num2 + num1); if (x == "-") st.push(num2 - num1); if (x == "*") st.push(num2 * num1); if (x == "/") st.push(num2 / num1); } else { st.push(stol(x)); } } return st.top(); } };
在上述代码中 我们使用了四个if语句来判断运算符是哪种类型
在学习了function包装器之后我们可以换一种思路
使用map容器来绑定字符串和function对象 使每个运算符对应一个function对象
每次遍历的字符串都在map容器中查找 如果找到就是运算符开始运算 如果找不到就不是运算符 直接入栈
改进后的代码如下
class Solution { public: int evalRPN(vector<string>& tokens) { stack<int> st; map<string,function<int(int,int)>> solmap; solmap["+"] = [](int x,int y){return x + y;}; solmap["-"] = [](int x,int y){return x - y;}; solmap["*"] = [](int x,int y){return x * y;}; solmap["/"] = [](int x,int y){return x / y;}; for (auto x : tokens) { auto it = solmap.find(x); if (it == solmap.end()) { st.push(stoi(x)); } else { long right = st.top(); st.pop(); long left = st.top(); st.pop(); auto e = solmap[x](left,right); st.push(e); } } return st.top(); } };
看起来代码好像没有精简多少 这是因为运算符的个数太少了 当运算符的数目很多时 这种方式对于代码精简的作用才会显示出来
function包装器的意义
- 将可调用对象的类型进行统一 便于我们对其进行统一化管理
- 包装后明确了可调用对象的返回值和形参类型 更加方便使用者使用