C++进阶(6)——lambda表达式
目录
基本概念
lambda表达式的语法
捕捉列表
lambda表达式的具体应用
lambda表达式的实现原理
基本概念
我们的lambda表达式本质上就是一个匿名函数对象,和我们的普通函数不同的是他是可以定义在函数的内部的,其实不光光我们的C++支持了lambda表达式,我们的很多的高级语言(Java, Python, C#)都是支持的,而且这一语法是使用频率比较高的语法之一,它可以提高我们的代码可读性,下面我们就来看看吧。
lambda表达式的语法
[capture-list] (parameters)-> return type {function body
}
相关说明:
[capture-list]:捕捉列表,这个列表出现在我们的lambda函数的开始位置,编译器根据[ ]来判断接下来的代码是不是lambda函数,捕捉列表能够捕捉我们上下文中的变量以供lambda函数使用,捕捉列表可以传值或是传引用捕捉,即使列表为空也是不可以省略的(重点)。
(parameters):参数列表,和我们的普通参数列表的功能相类似,如果不要传递参数,就可以同我们的( )一起省略掉。
->return type:返回值类型,用来追踪返回类型形式声明函数的返回值类型,没有返回值的时候这个部分可以省略掉,一般的返回值类型明确的情况下,也是可以省略的,可以由编译器对返回值类型进行自我推导。
{function body}:函数体,函数体的实现和我们的普通函数完全类似,除了可以使用其参数外,我们还可以使用我们上面捕获到的变量,函数体不可以省略。
根据我们上面的介绍,我们可以写出我们最简单的lambda表达式了,虽然这个代码什么也没干:
#include <functional>
#include <iostream>
using namespace std;
int main() {[]{};return 0;
}
接下来我们就可以简单地使用一下我们的lambda表达式了:
示例代码:
#include <iostream>
using namespace std;
int main() {auto add = [](int x, int y)->int {return x + y;};cout << add(1, 2) << endl;auto func = [] {cout << "hello world!" << endl;};func();int a = 1, b = 2;auto swap = [](int& x, int& y) {int temp = x;x = y;y = temp;};swap(a, b);cout << a << ":" << b << endl;return 0;
}
测试效果如图:
捕捉列表
我们的lambda表达式默认只是泗洪lambda函数体和参数中的变量,想使用我们的外层变量就要进行捕捉操作了。我们这里有常见的三种捕捉方式:
第一种方式:
在捕捉列表中显示的传值或是传引用捕捉,多个变量就用逗号分隔。
[x, y, &z]:x和y是值捕捉,z是引用捕捉。
第二种方式:
我们在捕捉列表中可以显示那就可以隐式捕捉,有两种:
[=]:捕捉值
[&]:捕捉引用
这样我们的lambda表达式用了什么变量,编译器就会自动捕捉那些变量了。
第三种方式:
我们还可以将上面的两种方式混合起来。
[=, &x]:表示的是其他变量使用隐式值捕捉,但是x引用捕捉。
[&, x, y]:表示的是其他变量使用隐式引用捕捉,但是我们的x和y使用值捕捉。
这里要注意的是,我们的第一个元素必须是&或是=,是&的时候后面就必须是值捕捉,是=的时候后面就必须是引用捕捉。
敲黑板:
1、lambda表达式如果是在函数的局部域中,它可以捕捉lambda位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量本身也不需要捕捉,lambda表达式中可以直接使用,这也就意味着lambda表达式如果是定义在全局的位置,那么我们的捕捉列表就必须要是空的。
2、默认情况下,我们这里的lambda捕捉列表是被const修饰的,也就是说我们传值捕捉来的对象是不可以被修改的,我们可以使用mutable加在参数列表的后面来取消其常量性,也就是说我们这个时候的传值捕捉对象就是可以修改的了,但是修改的还是形参对象,并不会影响实参。使用这个修饰符后,参数列表就不可以省略了(即使参数为空)。
代码示例:
#include <iostream>
using namespace std;
int x = 0;
// 这里是全局,没有什么可以捕捉的变量
auto func1 = []() {x++;
};
int main() {int a = 0, b = 1, c = 2, d = 3;auto func2 = [a, &b] {// 值的捕捉不可以修改,但是引用捕捉的变量是可以修改的// a++; // 报错b++;int ret = a + b;return ret;};cout << func2() << endl; cout << b << endl;cout << "*********************" << endl;// 隐式值捕捉auto func3 = [=] {int ret = a + b + c;return ret;};cout << func3() << endl;cout << "*********************" << endl;// 隐式引用捕捉cout << a << " " << b << " " << c << " " << d << endl;auto func4 = [&] {a++;c++;d++;};func4();cout << a << " " << b << " " << c << " " << d << endl;cout << "*********************" << endl;// 混合捕捉1cout << a << " " << b << " " << c << " " << d << endl;auto func5 = [&, a, b] {// a++; // 报错// b++; // 报错c++;d++;};cout << a << " " << b << " " << c << " " << d << endl;cout << "*********************" << endl;// 混合捕捉2cout << a << " " << b << " " << c << " " << d << endl;auto func6 = [=, &a, &b] {a++;b++;// c++; // 报错// d++; // 报错};func6();cout << a << " " << b << " " << c << " " << d << endl;cout << "*********************" << endl;// 局部静态变量和全局变量不能捕捉,也不需要捕捉static int y = 2;auto func7 = [] {int ret = x + y;return ret;};cout << func7() << endl;cout << "*********************" << endl;// 传值捕捉是拷贝默认是被const修饰的// 使用mutable可以消去const属性,但是我们的改变并不会影响外面的值cout << a << " " << b << " " << c << " " << d << endl;auto func8 = [=]()mutable {a++;b++;c++;d++;return a + b + c + d;};cout << func8() << endl;cout << a << " " << b << " " << c << " " << d << endl;cout << "*********************" << endl;return 0;
}
测试效果如图:
lambda表达式的具体应用
我们在学习使用lambda表达式之前,我们使用的可调用对象就是函数指针和仿函数对象了,这两种方式都比较的麻烦,于是我们的就可以使用lambda表达式,不仅方便还具有很高的代码可读性。
我们在实现一些比较器的时候,使用lambda表达式就会比较方便,我们这里还是来举个栗子:
代码如下:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Goods {string _name; // 商品名double _price; // 商品价格int _evaluate; // 商品评价// 构造函数Goods(const char* name, double price, int evaluate):_name(name),_price(price),_evaluate(evaluate){}
};struct ComparePriceLess {bool operator()(const Goods& g1, const Goods& g2) {return g1._price < g2._price;}
};struct ComparePriceGreater {bool operator()(const Goods& g1, const Goods& g2) {return g1._price > g2._price;}
};int main() {vector<Goods> v = {{"苹果", 1.2, 5}, {"香蕉", 3.3, 3}, {"橘子", 2.3, 4}, {"栗子", 1.1, 5}};// 使用我们的仿函数// 按价格升序sort(v.begin(), v.end(), ComparePriceLess());// 按价格降序sort(v.begin(), v.end(), ComparePriceLess());// 使用我们的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;});return 0;
}
lambda表达式的实现原理
其实聪明的友友可能已经猜到了lambda表达式的实现原理,它的实现原理其实和我们之间讲C++11中的for一样,编译之后从汇编的角度看,就没有什么范围for,范围for的底层就是调用迭代器,我们的lambda也是一样的,底层其实就是生成了对应的仿函数的类。
重点说明一下:
我们这里的底层仿函数是编译的时候按照一定的规则自动生成的,保证了不同的lambda表达式的类名是不一样的,lambda的参数/返回值类型/函数体其实就是我们的仿函数operator()的参数/返回值类型/函数体,lambda表达式的捕捉列表的本质就是生成仿函数的成员变量,也就是说我们的捕捉列表的变量都是lambda类构造函数的实参,隐式捕捉就要看传入了什么变量了。
我们这里还是来写个代码通过反汇编来验证一下:
代码示例:
#include <iostream>
using namespace std;
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.21;// 使用lambda表达式auto r1 = [rate](double money, int year) {return money * rate * year;};r1(10000, 3);// 使用函数对象Rate r2(rate);r2(10000, 3);return 0;
}
我们将这个代码转到反汇编来看:
我们通过下面这个图可以看到,我们一开始调用了Rate类的构造函数,然后我们在使用对象r2的时候调用了Rate类的()运算符重载函数。
同样的,如果我们将lambda表达式的代码也转到反汇编,我们可以看到这里的反汇编和上面的是很类似的,首先是调用了<lambda_uuid>类的构造函数,然后在使用对象r1的时候,就会调用<lambda_uuid>类的( )运算符重载函数。
我们这里为了严谨,将我们的可以验证一下我们不同的lambda表达式的类型名是不是一样的:
示例代码:
#include <iostream>
using namespace std;
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;auto r1 = [rate](double money, int year) {return money * rate * year; };auto r2 = [rate](double money, int year) {return money * rate * year; };cout << typeid(r1).name() << endl;cout << typeid(r2).name() << endl;return 0;
}
测试效果:
我们这里的结果是缩写的结果,但是还是显示了它们的不同之处。
敲黑板:
我们这里的类名处理成了<lambda_uuid>,这里的uuid即使我们的通用唯一识别码(Universally Unique Identifier),这个码在编程中经常被使用,这个码可以保证我们的类名唯一性。