目录
前言
一.lambda的引入
二、lambda函数的使用
1.一般使用
2.引用
三、包装器
1.包装普通对象
2.包装类成员对象
3.bind
前言
学习过python的同学应该对lambda函数不陌生,这是一个匿名函数,不需要写函数的名字。在不会多地方调用某个简单函数的地方,就可以使用lambda。
一.lambda的引入
在学习lambda函数之前,我们来看一个用例。这是一些商品,我们需要对商品进行排序。
struct Goods
{string _name; // 名字double _price; // 价格int _evaluate; // 评价Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};
int main()
{vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };sort(v.begin(), v.end());sort(v.begin(), v.end());
}
由于商品是自定义类型,比较函数要我们自己写,在Goods类里面写operator>很不方面。
- 无法使用sort函数,需要自己写排序,
- 排序的方式有很多,比如价格升序和降序,评价的升序和降序。
因此一般情况下我们都会写仿函数来帮助我们进行比较。例如下面两个仿函数
struct CompareEvaluateLess
{bool operator()(const Goods& gl, const Goods& gr){return gl._evaluate < gr._evaluate;}
};
struct ComparePriceGreater
{bool operator()(const Goods& gl, const Goods& gr){return gl._price > gr._price;}
};
给sort函数传递仿函数就可以按照我们的想法进行排序了。
随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法, 都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名, 这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。
二、lambda函数的使用
1.一般使用
lambda使用方法如下,有点长,先不用看,直接看后面的例子
lambda书写格式:[capture-list] (parameters) mutable -> return-type { statement }
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来 判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda 函数使用。
- (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以 连同()一起省略
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
举个例子,如下,[]为捕捉列表(先不捕捉,后续会讲),(int x)为参数,->int返回类型为int,{}里面存放函数内容。
[](int x)->int {cout << x << endl; return 0; };
如下两个方法可以进行lambda函数的调用,
第一个是直接在后面给参数调用。
第二个是赋值给auto 变量,再使用该变量名进行调用。
对于之前商品排序,现在我们也会修改了,一下子就搞定了。
2.引用
方法一:参数传引用,这是我们熟悉的方法,由于不需要返回参数,因此省略->()。如下
方法二:捕获列表
使用正常捕获,是const的无法修改,需要加mutable变为可修改(见二、1使用方法第三条)。使用引用捕获,不加mutable也可以修改(引用的目的大多是为了修改,这里编译器做了特殊处理)。如下引用捕获了x和y,同时也不需要传参了,因为我们使用了捕获列表,参数列表没有参数。
引用捕获列表还可以使用&,代表引用捕捉当前作用域中的所有变量,如下(这里只有x,y)。
三、包装器
1.包装普通对象
function包装器也叫作适配器。他的主要目的是为了包装可调用对象,主要的可调用对象有函数指针,仿函数,lambda。
为什么要包装可调用对象呢?我们来看看他们的弊端。
函数指针用起来太不方便了,写起来很难受。
仿函数需要在全局定义,不够简洁和美观。
lambda类型是匿名的,一般取不到类型。
如果现在我想像cmd命令一样,输入一个指令,计算机进行相应的操作,这个操作就可以是相关的函数,那么我可以进行如下定义。使用map的operator[]进行相关操作,红色方框的类型应该填什么呢?写函数指针和仿函数很不方便,写lambda类型都没有没办法写。这时就需要包装器上场了。
比如我要进行数字的加减乘除操作,我们可以这样传递第二个参数function<double(double a,double b)>,这样代表包装的可调用对象的类型。那么我们实际传参时,只要类型相同,既可以传递函数指针,又可以传递仿函数,还可以传递lambda匿名对象,这样非常方便。(注意添加头文件#include <functional>)如下所示
2.包装类成员对象
代码如下
class Add
{
public:static int addi(int a, int b){return a + b;}double addb(double a, double b){return a + b;}
};int main()
{function<int(int, int)> f1 = &Add::addi;cout << f1(5, 3) << endl;
}
包装类里面的静态函数,指定类域即可直接包装,&最好填,也可以不填。
如果是非静态呢? 由于类的非静态变量有this指针,因此这里编译不通过。
我们可以给第一个参数传递类指针,再定义一个类对象,取地址传过去就可以了。
还有一种写法,算是编译器的特殊处理,可以不用再生成类对象。
但是始终这个方法不太好,本来我就只想传两个参数,你一定要让我传第一个一直固定的,不是多此一举嘛,这时bind函数就出场了。
3.bind
bind也是在头文件<functional>里面的,他的作用是绑定函数,让函数参数变成我们想要的个数和顺序。
如下,绑定了一个减法lambda函数,第一个参数使用placeholders作用域_2(代表前面那个函数的第二个参数),第二个参数为该作用域下的_1(代表前面那个函数的第一个参数)。相当于10给到了y,3给到了x,因此输出结果是-7。
如果我们给lambda函数第一个参数x为固定值,如下给20,那么我们需要将前面function的参数个数减少一个,同时调用的时候也只需要传一个参数,也就是placeholders::_1(因为只有一个参数)。
当然第二个参数也可以给固定值。
学会了基本用法,我们回过头来修改类的成员函数。将function只设置两个参数,同时将Add::addb函数的第一个参数固定死传Add(),后续再传placeholders::_1, placeholders::_2,代表实参的第一个和第二个就好。
这样就可以按照我们的想法进行传参了。
谢谢大家观看