前言
我们目前已经介绍了三种可调用的对象,1、函数指针 2、仿函数 3、lambda表达式;但是这三都是不同的类型,无法做到调用时的类型的统一!如何使得相同返回值和参数的调用对象以相同的方式调用呢?这就是我们介绍的包装器!
目录
一、function
function包装器,又称适配器。和我们当时STL栈和队列的那个适配器一样!C++中的function本质是一个类模板!
我们先来看看,没有包装器存在的问题:
template<class F, class T>
T Func(F f, T x)
{
static int cnt = 0;
cout << "cnt: " << ++cnt << endl;
cout << "&cnt: " << &cnt << endl;
return f(x);
}
// 函数指针
int f(int d)
{
return d + 2;
}
// 仿函数
struct Functor
{
int operator()(int i)
{
return i + 2;
}
};
int main()
{
// 函数指针实例化
cout << Func(f, 3) << endl << endl;
// 仿函数实例化
cout << Func(Functor(), 3) << endl << endl;
// lambda
cout << Func([](int i) {return i + 2; }, 3);
return 0;
}
这里它实际上是将上面的模板实例化了三份:
这三个函数的参数,返回值都一样,只是他们的类型不一样,这就导致了,调用时实例了三份(静态变量有三份,地址不同)!如果他们的类型是一样的,则模板只需要实例一份即可!这就大大的提高了使用模板的效率!如何实现呢?当然是使用function包装喽:
包装可调用函数
我们先来介绍一下function的原型, 他在<functional>的头文件中:
template <class T> function; // undefined
template <class Ret, class... Args> class function<Ret(Args...)>;
Ret :被调用函数的返回值类型; Args... :被调用函数的形参;
OK,有了function我们就可以对上述的,参数和返回值都一致,但是类型不一致的三个函数进行统一封装成,相同的类型:
int main()
{
// 函数指针实例化
function<int(int)> f1 = f;
cout << Func(f1, 3) << endl << endl;
// 仿函数实例化
function<int(int)> f2 = Functor();
cout << Func(f2, 3) << endl << endl;
// lambda
function<int(int)> f3 = [](int i) {return i + 2; };
cout << Func(f3, 3) << endl;
return 0;
}
这里我们明显的看到他们的静态变量只有一份,且地址都是同一个!上面我们知道他们不是统一的类型,但是这里如何验证他们的类型一致呢?其实很简单,我们只需要使用typeid即可:
包装成员函数
function不仅可以包装,函数指针、函数对象(仿函数)、lambda;还可以包装成员函数:
成员函数又分为两种:1、静态成员函数 2、非静态成员函数
静态的包装就很简单了:
• 静态成员函数没有this指针,可以和上面的普通函数指针一样用,但注意的是需要指定类域!
它的包装格式如下:
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
int plusd(int a, int b)
{
return a + b;
}
};
int main()
{
// 包装静态成员函数
function<int(int, int)> f = Plus::plusi;
// function<int(int, int)> f = &Plus::plusi; // 静态这里有无&都可以
cout << f(3, 5) << endl;
return 0;
}
• 非静态的成员函数包装稍有点复杂,因为非静态成员的函数需要对象/对象的指针调用,所以得把对象/对象的指针传递过去,注意:这里也是需要指定类域的,且非静态成员函数必须加&
非静态成员的包装格式如下:
// 包装非静态的成员函数
function<int(Plus*, int, int)> f2 = &Plus::plusd;// 传递对象的指针
Plus plus;
cout << f2(&plus,3, 5) << endl;
// cout << f2(Plus(),3, 5) << endl; // error不能传递匿名对象
function<int(Plus, int, int)> f3 = &Plus::plusd;// 传递对象
cout << f3(plus, 3, 5) << endl;
cout << f3(Plus(), 3, 5) << endl;
一些参数包的问题
• 可不可以将对象/对象的指针设置为引用?
不可以的!首先你设置左值引用的话匿名对象就引用不了了!
• 设置成右值引用,左值就无法传递了!
• 设置成const 左值引用呢?
不行,这样写直接无法包装了!也就是无法构造function对象了!
• 那可不可以给搞成万能引用呢?
这个更不行,万能引用/易用折叠是函数模板的概念!
function的一些使用场景
这个题目我们以前是写过的,主要思想就是用栈!这里先回忆一下,以前的两种写法:
1、暴力判断+栈
思路:遍历字符串,如果是运算符,获取栈顶的操作数两次,然后计算;否则就是操作数,入栈!最后栈里面的那个元素就是最后的答案!
class Solution
{
public:
int evalRPN(vector<string>& tokens)
{
stack<int> st;
for(const auto& c : tokens)
{
// 运算符计算
if(c == "+" || c == "-" || c == "*" || c == "/")
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
if(c == "+") st.push(left + right);
else if(c == "-") st.push(left - right);
else if(c == "*") st.push(left * right);
else if(c == "/") st.push(left / right);
}
else // 数字入栈
{
st.push(stoi(c));
}
}
return st.top();
}
};
过了,但是这样呢写代码,属实不优雅!我们可以使用今天的包装器,将要执行的方法对应的函数提前处理好:
class Solution
{
public:
int evalRPN(vector<string>& tokens)
{
stack<int> st;
// 提前将所执行的方法和操作符建立映射
map<string, function<int(int,int)>> m = {
{"+", [](int left, int right){return left + right;}},
{"-", [](int left, int right){return left - right;}},
{"*", [](int left, int right){return left * right;}},
{"/", [](int left, int right){return left / right;}},
};
for(const auto& c : tokens)
{
// 运算符计算
if(m.count(c))
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
st.push(m[c](left, right));
}
else // 数字入栈
{
st.push(stoi(c));
}
}
return st.top();
}
};
可能这样写你觉得还不是和上面的也差不多嘛!但是你有没有想过,万一不是只有四个操作符,后还会增加呢?下面的写法就是把对应的操作符加上即可,上面的可就是一堆的暴力判断了!代码的鲁棒性(健壮性)明显提升了!
当然,function不知是在OJ中的使用,在十几种也有很多的使用场景,比如:即可哈希表/map与用户输入的指令,与对应方法的完美结合:
#include <unordered_map>
#include <string>
int main()
{
// 包装了返回值为 void,参数为 void 的函数类型
unordered_map<string, function<void(void)>> hash;
hash["下载请求"] = []()->void { cout << "正在进行下载任务..." << endl; };
hash["SQL查询"] = []()->void { cout << "正在进行SQL查询..." << endl; };
hash["日志记录"] = []()->void { cout << "正在记录日志信息..." << endl; };
string comm; // 指令
while (cin >> comm)
{
if (!hash.count(comm))
cout << "该指令不存在,请重新输入" << endl;
else
hash[comm](); // 调用函数
}
return 0;
}
这种设计在往网络中非常常见!也就是function是非常常用的!
二、bind
bind是一个函数模板,他就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来"适应"原对象的参数列表!
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);
这就是他的原型,Fn&& fn就是他接受的要包装/适配的目标函数;Args&&... args是生成新的可调用对象的参数列表!其中,这是是函数模板,所以&&都是万能引用!
bind的参数包
bind的参数包,是一个个的“整数”,其实他是一种占位符,通常是在 placeholders 的命名空间中!其中_1永远代表第一个bind的第一个参数;_n始终代表bind的第n个参数!
bind的使用
OK,我们还是用一下,首先使用一下改变原来参数的顺序:
int sub(int x, int y)
{
return x - y;
}
int main()
{
// 未改变原来参数的顺序
auto bfunc1 = bind(sub, placeholders::_1, placeholders::_2);
cout << bfunc1(5, 3) << endl;
// 改变原来参数的顺序
auto bfunc2 = bind(sub, placeholders::_2, placeholders::_1);
cout << bfunc2(5, 3) << endl;
return 0;
}
这里实现改变顺序的原因很简单,就是前面说的bind的参数是_1是默认只接受第一个参数,它的内部其实也是和上面的function一样实现了仿函数,再去拿着bind参数列表中的参数去调用目标函数了!这里由于bind中的_1 和 _2调换了,所以调目标函数的时候也就传反了!即实现了参数的顺序调整!
OK,这里的一般的对象也绑定了(仿函数和lambda同理),可不可以绑定成员函数有呢?当然是可以的:
class Test
{
public:
Test(int x) :_n (x){}
int sub(int x, int y)
{
return x - y;
}
private:
int _n;
};
int main()
{
// 未改变原来参数的顺序
auto bfunc1 = bind(&Test::sub, placeholders::_1, placeholders::_2, placeholders::_3);
Test test(1);
cout << bfunc1(&test, 5, 3) << endl;
// 改变原来参数的顺序
auto bfunc2 = bind(&Test::sub, placeholders::_1, placeholders::_3, placeholders::_2);
//Test test(2);
cout << bfunc2(Test(1), 5, 3) << endl;
return 0;
}
注意:这里一旦要绑定成员函数,就必须得传递对象的地址/对象,还是和上面的function一样成员函数是由对象/对象的指针调用的!由于要传递对象/对象的指针,所以第一个参数就是对象/对象的地址!
当然其实改变参数的顺序其实是具体没啥意义的!下面我们来看看改变参数的个数:
上面刚写的,这个绑定成员函数的,让人很不爽的一点就是得手动的传递对象/对象的指针,我不想传递,可不可以呢?当然是可以的,此时只需要将_1与一个对象/地址绑定死即可:
// 未改变原来参数的顺序
auto bfunc1 = bind(&Test::sub, &Test(1), placeholders::_1, placeholders::_2);
cout << bfunc1(5, 3) << endl;
// 改变原来参数的顺序
auto bfunc2 = bind(&Test::sub, Test(1), placeholders::_2 ,placeholders::_1);
cout << bfunc2(5, 3) << endl;
这样调用的时候就非常的爽了!如果你想后面的参数绑定死也是可以的:
当然你直接在bind把参数全部传递过去了,下面再调用就不在有效果了!
除了上述的,用auto推导类型以外,还可用function:
这里可以理解为又在bind上包装了一层!
OK,本期分享就到这里!我是cp我们下期再见~!