文章目录
引入
本文开始将介绍c++11引入的两个特别好用的内容,lambda对象和包装器。
lambda对象
第一个部分,我们先来看lambda表达式的知识点。
lambda表达式语法
lambda表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。lambda表达式语法使用层而言是没有没有类型,所以我们一般是用auto或者模板参数定义的对象去接收lambda对象。
lambda表达式的格式:[capture list] ->(参数列表) 返回值{//函数行为};
其实lambda都虽然是个对象,但是他可以像函数一样调用。就像我们实现的仿函数那样。
1.[capture list]参数捕捉列表,这个是用来捕捉lambda对象外的一些参数的。标志就是[]的出现,里面可以没有捕捉的参数。但是在定义lambda对象的时候,必须要有这个[]出现,哪怕是空的。这个参数捕捉列表现在是讲不明白的,我们先了解一下就好了,这个得等后面讲完使用和lambda对象底层原理后才能明白它的设计。
2.定义这个lambda对象就好像是在定义一个函数一样。有参数列表,有返回值。{}里面还有函数的行为。但是lambda对象还远远不止于此。
首先,参数列表中如果没有参数,是可以把这个参数列表整个给省略的,结构就变成这样:
[capture list] -> 返回值{//函数行为};(无参调用)
其次,这个返回值也可以省略掉,那有返回值怎么办呢?只需要在函数体内自行返回。编译器会自动识别返回类型的。结构变成这样:[capture list] (参数列表) {//函数行为};
lambda对象的使用
前面光说不写肯定是不好理解的,所以在这个部分就举一些例子讲解一下使用。’
int main() {
//使用格式
//lambda对象的类型自己很难控制 干脆就
auto l1 = [](int a, double b)-> double
{
double ret = (double)a + b;
return ret;
};
cout << l1(3, 4.5) << endl;
return 0;
}
必须要明确的一个点是,lambda本质上是一个对象,而不是函数。只不过它的行为像函数,而且可以通过这个对象调用类似于仿函数中operator()的内容,实现一些简单的功能。只不过说相对于仿函数有具体的类型(仿函数类),这个lambda对象是很难手动控制它的类型的,就算能写也是很复杂,所以方便起见,直接用auto接收就好了。这个是c++11的特点。
我们试着把返回值省略一下:
不是只删掉返回值类型double,要把->一起删了。还是可以正常使用的。
再把参数列表给它扣了,发现还是可以正常调用的。
此外,还有个隐藏的好处就是,lambda对象可以定义在函数内。以前我们在定义一个函数的时候,函数内部是不能嵌套定义的另一个函数的。但是可以在函数内嵌套定义一个lambda对象。这就方便很多了。因为很多情况下,我们希望有个函数可能是特供给某个函数内部使用的,这个时候用lambda对象就非常爽了。
不过lambda对象一般来说,都是定义一些轻量级的函数。如果函数体太大其实是不好看的,而且一般也不这么用。如果只是短短的几行用起来就很爽。
lambda对象使用的时候遇到的问题
当然,lambda对象的使用也不是这么简单的。还是有一些细节的问题。
- lambda对象不能使用lambda对象外的非全局变量
听着很绕口,还是实践检验一下:
比如我们在main函数中定义了两个变量a、b。想要在lambda对象内使用。但是这样子是会报错的。虽然编译器会向上查找,但是对于lambda对象而言,他不会越过lambda对象范围的限制找到这个a和b。
但是全局变量确实可以使用:
全局变量不一样。全局变量毕竟定义在全局,本身就是哪里都可以用的。从内存的角度去理解就是,全局变量c存储在数据段,无论在哪里都可以访问。而a和b存储在栈区,lambda对象也是在栈区。栈区会有栈帧访问的限制。
当然是有办法使用到a和b的,就是用到lambda对象前面的捕捉参数列表[capture list],但是在这里先不讲,等下再说。
- lambda对象不能使用递归
这也是lambda对象和函数的比较大的一个区别。在以往学习的函数体内,是可以自己调用自己的。这种行为也被称为递归。但是lambda是不行的,尝试着看看:
我就不设置什么递归终止条件了。直接在内部调用一下,发现是会报错的。没有初始化前是没办法使用的。这进一步地作证了,lambda对象本质上是个对象。不是函数。对象即对象变量,才会有初始化这么一个说法。
但是我们再反过来看使用场景,本身lambda对象就是用来写一些轻量级的,简单的函数。使用场景就没有那么复杂。真的要使用递归那就老老实实写个递归的函数就好了。
捕捉参数列表
前面我们说到,lambda对象内没法使用除了全局变量以外的变量。但是可以通过捕捉参数列表解决。这个部分,我们来重点讨论一下参数捕捉列表的一些相关内容。
使用规则如下:
1.lambda表达式中默认只能用lambda函数体和参数中的变量,如果想用外层作用域中的变量就需要进行参数捕捉。
2.第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量⽤逗号分割。[x,y, &z] 表示x和y值捕捉,z引用捕捉。
3.第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写一个=表示隐式的值捕捉,在捕捉列表写一个&表示隐式的引用捕捉,这样我们 lambda表达式中用了那些变量,编译器就会自动捕捉那些变量。
3.第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[=, &x]表示其他变量隐式值捕捉,x引用捕捉;[&, x, y]表示其他变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第一个元素必须是&或=,并且&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后⾯的捕捉变量必须是引用捕捉。
4.lambda表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使用。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
5.默认情况下, lambda捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,mutable加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。
使用规则看着多,其实十分好理解和记忆,我们一个一个看。
捕捉参数列表的使用
首先,我们搞清楚传值捕捉和引用捕捉:
这个场景中,在lambda对象外定义了三个变量。a和b直接放在捕捉参数列表中,这个是传值捕捉。str这个string类以&str的形式放在捕捉参数列表中,这不是传地址,这个在捕捉参数列表中是引用捕捉,这是要注意的。
然后我们试着在lambda对象L中使用一下这个三个变量,发现是可以使用的。只不过说,对于传值捕捉的参数a和b,如果不加关键字mutable修饰的情况下,是具有const性的。也就是说在lambda对象内无法直接修改传值捕捉后的参数。
但是引用捕捉的是可以被修改的。
但是可以使用关键字mutable取消传值捕捉的const性质:
这样子就不会报错了。但是需要注意使用该修饰符后,参数列表不可省略(即使参数为空)。
捕捉参数列表是没办法捕捉到全局变量和静态变量的。但其实也不需要去捕捉,这两个数据放在数据段和静态区,lambda对象内是可以直接使用的。所以不存在捕捉这么一说。
当然也可以是一把捕捉过来:
这里直接在参数列表中放个&,代表全部引用捕捉。只不过要注意的是,并不是说真的一把把外面所有的变量捕捉过来。而是里面用到哪个捕捉哪个。
也可以混合捕捉:
这里其它的都是引用捕捉,但是唯独a是传值捕捉。
但是这里有一点要注意的是:
如果使用混合捕捉了,捕捉列表的第一个位置必须是=或&。这是语法规定,且第一个如果是&,后面只能传值捕捉。反之只能是引用捕捉。
不能写成这样:&, =和=, &
因为这样子会造成歧义,编译器识别不出来的到底是哪种捕捉方式的。
lambda的使用
我们这里来感受一下lambda的使用的好处:
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
// ...
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
struct ComparePriceLess{
bool operator()(const Goods& gl, const Goods& gr){
return gl._price < gr._price;
}
};
struct ComparePriceGreater{
bool operator()(const Goods& gl, const Goods& gr){
return gl._price > gr._price;
}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "⾹蕉", 3, 4 }, { "橙⼦", 2.2, 3
}, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
return 0;
}
假设想给商品按照价格排序,我们可以使用算法库< algorithm >里面自带的sort函数。但是默认是排降序的,但那是针对于整形而言的。我们现在想要按照价格来排序,所以需要自行实现两个仿函数,然后传给sort的第三个参数。这是我们以前的做法。
但是商品如果是按照名称排序呢?又或是按照评分排序呢?又得写对应的仿函数。这太麻烦了。这个时候lambda对象就派上用场了。
在这里先说一点:lambda对象也是可以像仿函数那样使用的。所以我们可以把所有的比较逻辑的仿函数全部换成lambda对象。这个对象可以传匿名的,也可以是有名字的。为了写的简单点,我们可以直接传匿名对象过去:
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._price < g2._price;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._price > g2._price;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate < g2._evaluate;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate > g2._evaluate;
});
直接把整个lambda表达式当作一个对象传给sort的第三个参数(比较逻辑)。这样子就不用写仿函数了,因为仿函数写起来还是比较复杂。而且还有取名字。
但是使用lambda对象就简单的多了,不用想方设法地取名字,也不用实例化出来。直接把表达式实现好传进去就可以了。这里我就不展示输出结果了,感兴趣地可以拷贝过去看。
但是lambda对象可以这么用是有原因的。具体是什么原因这个放在lambda的原理部分讲解。
lambda的原理
在前面我们也是初步的看到了lambda对象的使用是非常爽的。但是它的原理到底是什么呢?为什么它可以像仿函数一样使用呢?这里我们一起来看下。
还记得c++11引入的范围for吗?范围for看着很高级,但是本质上就是个迭代器。只不过上层封装了一下辨别不出来而已。
lambda对象其实和这个范围for的性质很像,也是被封装过的。其实lambda的底层就是一个仿函数。能够通过lambda对象调用函数其实也就是lambda对象底层实现了operator(),也就说我们写了⼀个lambda 以后,编译器会生成⼀个对应的仿函数的类。
这个仿函数的类名是编译按一定规则(uuid)生成的,保证不同的lambda生成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是lambda类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传那些对象。
为了验证上面这些原理真假,我们下面给段代码证明一下:
class Rate{
public:
Rate(double rate)
: _rate(rate)
{}
double operator()(double money, int year){
return money * _rate * year;
}
private:
double _rate;
};
int main() {
double rate = 0.12;
double money = 10000;
int year = 5;
//调用仿函数
Rate R1(rate);
//调用lambda对象
auto R2 = [rate](double money, int year) ->double
{return money * rate * year; };
R1(money, year);
R2(money, year);
return 0;
}
我们进入调试的反汇编界面查看:
//仿函数对象
Rate R1(rate);
00007FF7F43F533E movsd xmm1,mmword ptr [rate]
00007FF7F43F5343 lea rcx,[R1]
00007FF7F43F5347 call Rate::Rate (07FF7F43F10C8h)
//很明显 这里是调用了Rate的构造函数进行构造
//lambda对象
auto R2 = [rate](double money, int year) ->double {return money * rate * year; };
00007FF7F43F534C lea rdx,[rate]
00007FF7F43F5350 lea rcx,[R2]
00007FF7F43F5357 call `main'::`2'::<lambda_1>::<lambda_1> (07FF7F43F17A0h)
//这里的lambda对象名字是<lambda_1>,确实可以看的出来是随即生成的
//也是把rate这个变量传进来进行构造
//
//虽然汇编代码的语句不一样。但其实本质上是一样的,都是在构造一个类。
//
//调用部分
R1(money, year);
00007FF7F43F535C mov r8d,dword ptr [year] //传入变量year
00007FF7F43F5360 movsd xmm1,mmword ptr [money] //传入变量money
00007FF7F43F5365 lea rcx,[R1]
00007FF7F43F5369 call Rate::operator() (07FF7F43F13CFh) //调用operator()
R2(money, year);
00007FF7F43F536E mov r8d,dword ptr [year] //传入变量year
00007FF7F43F5372 movsd xmm1,mmword ptr [money] //传入变量money
00007FF7F43F5377 lea rcx,[R2]
00007FF7F43F537E call `main'::`2'::<lambda_1>::operator()
//调用operator()
我们发现,lambda对象和仿函数其实区别并不大。所谓lambda对象,其实就是编译器在底层自行生成了一个随即名称(基本保证不重复)的类,里面重载实现了operator() 而已,道理和仿函数是一眼的。
所以我们可以认为,捕捉参数列表其实就是lambda对象在底层的仿函数类的成员变量。
包装器
本部分将介绍第二个c++11中重要的内容——包装器。
包装器分为两个,一个叫做function,另一个叫做bind。
function
其实包装器function就是一个类模板。
template <class T>
class function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
之所以叫它包装器,是因为std::function的实例对象可以包装存储其他的可以调⽤对象,包括函数指针(函数名)、仿函数、 lambda 、 bind表达式等。
这些被包装的东西都有一个特性,就是可以使用名字()去调用。比如可以通过函数指针加括号传值调用函数,可以通过仿函数对象名去调用内部的operator(),可以通过lambda对象的名字去调用。
而std::function这个类就是用来包装这些可调用对象的。它被定义在了头文件< functional >中,使用这个类模板的时候需要包这个头文件。
function的使用
我们先来看看基础语法是怎么用的:
使用语法:function<可调用对象的返回值(可调用对象的参数列表)> function类对象名 = 可调用对象
我们举个例子看看:
#include<functional>
int main() {
auto L = [](int x, int y) ->int {return x + y; };
//直接调用
cout << add(1, 2) << endl;
cout << M()(1, 2) << endl;
cout << L(1, 2) << endl;
//包装后调用
std::function<int(int, int)> f1 = add;
std::function<int(int, int)> f2 = M();
std::function<int(int, int)> f3 = L;
cout << f1(1, 2) << " " << f2(1, 2) << " " << f3(1, 2) << endl;
return 0;
}
我们实现的三个可调用对象都是返回值int,需要传两个int参数进行调用的。所以在实例化function的时候,在模板参数的位置是< int(int, int) >
这个是需要一一对应的。function去包装一个可调用对象的时候,不是一定要接收左值包装的。也可以是临时对象。也就是说:包装仿函数的时候,可以给具体的对象,也可以是匿名的对象。包装lambda对象的时候,也是可以直接将lambda表达式直接给过去的。
但是还有一种可调用对象需要我们特别的注意一下,就是类里面的成员函数。分为静态成员函数和非静态成员函数。
首先回顾一下这两个函数有什么区别——非静态成员函数的参数列表中,第一个参数是隐式的this指针,但是静态成员函数是没有的。
正是因为类的成员函数有可能会有隐式this指针,使用function包装实例化的时候,又是需要返回值、参数列表一一对应的。所以这就导致了,如果funtcion包装的是一个非静态的成员函数,那就需要传this指针的参数进去进行实例化。
我们现在试着包装一下类的成员函数:
class A {
public:
void Print(int x = 1) {
cout << "Print(A* const this, int x) " << x << endl;
}
static void Test(double y = 2.5) {
cout << "Test(double y) " << y << endl;
}
private:
int _a = 1;
int _b = 2;
};
int main() {
//包装静态成员函数
function<void(int)> f1 = &A::Test;
f1(2.5);
//包装非静态成员函数
//直接把A*传给this指针接收就可以了
//如果是const A* const this指针也可以接收 权限缩小而已
function<void(A*, int)> f2 = &A::Print;
A a;
f2(&a, 3);
return 0;
}
输出结果:
但是对于与包装非静态成员函数的时候,其实不是一定要传指针给this指针的,也可以传对象:
甚至是可以传引用:
稍微解释一下:
我们再调用一个类的成员函数的时候,有两种方式:
1.通过对象和操作符.直接调用
2.通过指向对象的指针间接调用
这两种操作都是可以的,本质都是把对象的地址传给成员函数中的this指针。所以上面无论function中怎么样声名,都是传地址啊。
如果是声名的指针A*,那就直接把地址给过去了。如果传的是引用或者传值,就是把这个对象的地址给过去。所以传地址传引用都是可以的。这个需要结合以往的知识去理解。
function的应用场景
很多人可能会觉得,这不是脱了裤子放屁吗。明明可以直接调用,但是为什么要把他们包装起来呢?这太麻烦了。
别急,我们现在来看看一个简单的应用场景:
150. 逆波兰表达式求值 - 力扣(LeetCode)
这题其实就是把一个数学的后缀运算表达式计算出结果。
比如我们常见的3 + 5这是中缀,因为运算符在中间。把它转化为后缀就是3 5 +。
这么做的原因是因为计算机很难处理中缀表达式。因为运算符有优先级。而后缀表达式经过处理,按照一定的方法一定是可以直接从头到尾线性的计算出正确的答案的,符合运算符的逻辑。至于怎么转成中缀的不是重点。重点是我们应该通过function改进代码:
从后缀表达式计算出结果很简单:
遍历表达式,碰到操作数就入栈st。碰到操作符,就取出栈顶的最上面两个元素出来计算(先取出来的是右操作数),算完之后重新入栈st。直到序列遍历完成。
代码如下:
#include<vector>
#include<stack>
#include<string>
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();
}
};
但是有没有发现这样子是有一个弊端的,就是每次都需要去判断是什么操作符。如果再多加点运算符,就得多加几个case判断。这种写起来是很累的。
我们就在想,能不能一看到符号就直接调用它对应的操作。这不正好是映射关系吗?那就直接使用map这个容器就好了。但是现在面临着第二个问题,对应的操作是函数。应该怎么样作为类型传进去给map的value呢?一般来说可以使用函数指针,但是函数指针很难用,c++中也不喜欢用这种方式。
这不正好用到了我们刚刚学的包装吗?而且加减乘除这个函数特别短小,又可以直接用lambda对象来写,这就很轻松了,我们看看代码是怎么写的:
class Solution {
public:
int evalRPN(vector<string>& tokens) {
map<string, std::function<int(int, int)>> matchOpr = {
{"+", [](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;}}
};
stack<int> st;
for(auto str : tokens){
if(matchOpr.count(str)){
int right = st.top();
st.pop();
int left = st.top();
st.pop();
st.push(matchOpr[str](left, right));
}
else st.push(stoi(str));
}
return st.top();
}
};
对代码进行稍微的改进,我们发现简单明了了许多。使用map这个容器可以把运算符和对应的函数进行一个映射。对应的函数其实就是一个操作,使用包装器包装一下可以把所有的操作封装成同一个类型,放在map里面非常爽。
map是不支持冗余插入的。之前讲过,map是可以通过接口count来判断一个关键字是否存在于map中的。因为出现次数只可能是0或1。
而且在对操作数运算的时候,就方便太多了。就再也不用一个一个地去判断是哪个操作数了。直接取映射关系里找出来调用就好了。
注:这里地stoi是把一个string数字转化为整形int。
从这里就能看出来,其实包装器function还是很有意义的。这里只是简单的应用。在网络库里面,是有很多这样的映射关系的。比如一个指令对应一个操作,那么使用这样的map就可以极大的提高小笼包,就不用一个个地去判断什么操作了。
bind
接下来我们再讲解一个包装器——bind,也称绑定。
simple(1)
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);
bind是一个函数模板,它也是一个可调用对象的包装器,可以把他看做一个函数适配器,对接收的fn可调用对象进行处理后返回一个可调用对象。 bind 可以用来调整参数个数和参数顺序。bind也在这个头文件中。
调用bind的一般形式:
auto newCallable = bind(callable(被bind处理的可调用对象),arg_list);
其中newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是占位符,表示newCallable的参数,它们占据了传递给newCallable的参数的位置。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。_1/_2/_3…这些占位符放到placeholders的一个命名空间中。
简单来说就是,bind是一个函数适配器,对传入的可调用对象进行参数的控制。比如调整参数的顺序,和调整可传参的个数。这些参数的控制都是交给arg_list的一个参数列表去控制,且有固定的名字_n(n为一些整数)。
这些固定的名字都放在了一个命名空间placeholders里面:
直接看上面这些内容是很生涩和抽象的。但是不用急,我们后面一个一个讲。
bind的使用
这个部分我们来讲讲bind的使用,在理解调整参数这个功能之前,我们先来掌握基础用法。
首先,bind其实就是绑定一个可调用对象。这个可调用对象包括函数指针,仿函数、lambda对象。和function是一样的。
#include<functional>
#include<string>
int Sub(int a, int b){
return (a - b) * 10;
}
int SubX(int a, int b, int c){
return (a - b - c) * 10;
}
int main() {
//要使用bind就得使用参数列表_n
//放在命名空间placeholders里面
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
//auto newCallable = bind(callable,arg_list);
auto Bind1 = bind(Sub, _1, _2);
//这里的_1代表接受的第一个参数 ,_2代表接收到的第二个参数
cout << Bind1(10, 5) << endl;
return 0;
}
我们直接来看输出结果,答案是50。发现被绑定的Bind1其实也是个可调用对象。并且调用出来的结果和直接调用Sub是一样的。
这里是如何做到调用的呢?我们来分析一下:
首先,bind的语法意义是将一个可调用对象callable经过bind后给给到另一个可调用对象newCallable。但是这个newCallable的类型也是很难手动写出来的。所以这种情况通通用auto去接受就好了。
bind是一个函数模板,一般来说,函数模板是不需要我们进行显式实例化的。但是类模板是必须要的。所以在function的使用中我们需要手动的将被调用对象的返回值和参数类型个数传给function的模板参数。但是函数的一般来讲,这个活儿都是交给编译器去干的。
bind的第一个参数必须是被绑定的对象,如图中的Sub函数(其实传的是函数指针)。剩下的我管他叫bind参数列表。即_1, _2, _3…等一系列参数。一共有n个,这个n取决于被绑定的可调用对象callable的参数列表有几个参数。
比如这里的Sub有两个参数,所以在对Sub进行bind的时候,后面就只用写_1, _2。总而言之,有n个参数就是到_n。
然后通过Bind1(经过bind过的可调用对象)调用的时候,是按照顺序传过去的。
比如我们这里使用:Bind1(10, 5);,就是将10传给_1, 5传给_2。这是一一对应的。
然后_1又对应着Sub的第一个参数a,_2对应Sub的第二个参数b。
所以最后Bind1(10, 5) 等效于Sub(10, 5)
再举一个例子看看(对SubX函数进行bind):
但是这里要注意的是,这个_n的参数列表,必须是从1开始连续的数字
这个先当作一个结论记住,我们后面加将调整参数的时候会说。
bind调整可调用对象的参数
前文提到,bind最大的特点是调整一个可调用对象的参数顺序/个数。这到底是怎么实现的呢?我们一起来看看。
调整参数的顺序
调整参数顺序很简单,就是把_n的顺序换一下:
#include<functional>
#include<string>
int Sub(int a, int b){
return (a - b) * 10;
}
int SubX(int a, int b, int c){
return (a - b - c) * 10;
}
int main() {
//要使用bind就得使用参数列表_n
//放在命名空间placeholders里面
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
cout << "auto Bind1 = bind(Sub, _1, _2); " << endl;
cout << "auto Bind2 = bind(Sub, _2, _1); " << endl;
auto Bind1 = bind(Sub, _1, _2);
auto Bind2 = bind(Sub, _2, _1);
cout << "--------------------------------------------" << endl;
cout << "Bind1(10, 5) -> " << Bind1(10, 5) << endl;
cout << "Bind2(10, 5) -> " << Bind2(10, 5) << endl;
cout << "Bind1(5, 10) -> " << Bind1(5, 10) << endl;
cout << "Bind2(5, 10) -> " << Bind2(5, 10) << endl;
return 0;
}
改变参数的顺序,其实就是根据bind的传参原理进行修改:
我们发现,仅仅只是把bind中的_n参数列表改个顺序,就导致了传相同值的时候答案不同。
为什么会有这样的情况呢?我们就以Bind1(10, 5)和Bind2(10, 5)进行分析:
BInd1被绑定的时候,_1在前,_2在后,所以10传给_1后再传给a,5传给_2后再传给b,最后算出来的答案是50。
而Bind2被绑定的时候,前面还是按照顺序传的,所以10传给_2,5传给_1。但是这个_1,_2
的顺序是相对被绑定的可调用对象而言的。也就是说,_1必须传给callable的第一个位置的参数,_2必须传给callable的第二个位置的参数,_n必须传给callable的第n个位置的参数。
所以这就是调整参数的顺序。本质就是因为_n参数列表必须对应被绑定可调用对象去进行传参,但是调用newCallable的时候,又不是按照_n的数字顺序去对应接收传参。这就导致了参数的位置被调整了。
但是这个用途并不是很大,因为不太需要这样子做。
调整参数的个数(绑定参数)
真正用的多的还是这个用法,即调整参数的个数。也称绑死某个参数。
int main() {
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
auto Bind1 = bind(SubX, _1, 5, _2);
auto Bind2 = bind(SubX, 5, _1, _2);
auto Bind3 = bind(SubX, _1, _2, 5);
//20
cout << Bind1(10, 3) << endl;
//-80
cout << Bind2(10, 3) << endl;
//20
cout << Bind3(10, 3) << endl;
return 0;
}
我们来看一下输出结果:
在传递_n参数列表的时候,按正常来说,应该是从_1到_3,但是这里我们把一个位置的参数定死为5。这是合乎语法逻辑的。这种情况就叫他绑定参数,把被绑定对象的参数的某个位置给定住了。
还记得我们前面说的,_n参数列表必须是从_1开始连续的。所以在上面绑定了一个参数后,哪怕那个绑定的位置占在中间了,但是最后一个位置仍然是_2。
这里我们要记住:所谓的_n参数列表,它对应的数字是几,就代表着被绑定的可调用对象可以被我们传参的第几个位置。所以必须是按照顺序来的。这点要注意一下。
绑定参数的应用场景
假设现在有一个场景,要计算复利。
比如本金有若干种可能
对应的年化率是固定的,假设年化率为0.05
现在分别要算存储年限3年,5年,10, 30年的利息(利滚利的利息),我们应该怎么写代码?
//直接使用lambda对象就好了
auto func1 = [](double rate, double money, int year)->double {
double ret = money;
for (int i = 0; i < year; i++){
ret += ret * rate;
}
return ret - money;
};
但是现在有一个问题,就是利率不变,有可能有时候本金也是不变的。那不变的情况下,还要传入这个不变的参数岂不是特别麻烦?有没有办法不传呢?
有的,就用我们刚刚讲的绑定参数就可以了:
// 绑死⼀些参数,实现出⽀持不同年华利率,不同⾦额和不同年份计算出复利的结算利息
function<double(double)> func3_1_5 = bind(func1, 0.015, _1, 3);
function<double(double)> func5_1_5 = bind(func1, 0.015, _1, 5);
function<double(double)> func10_2_5 = bind(func1, 0.025, _1, 10);
function<double(double)> func20_3_5 = bind(func1, 0.035, _1, 30);
直接进行绑定,然后再让function对bind绑定后的表达式包装一下,这样子就能只传一个参数就可以了。这样就非常方便了。
至于具体的我就不再多说了,这里仅仅是介绍一下bind绑定参数的应用场景。实际上比这多得多。感兴趣的可以自行试验一下。