目录
基本概念
我们的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;
// 混合捕捉1
cout << a << " " << b << " " << c << " " << d << endl;
auto func5 = [&, a, b] {
// a++; // 报错
// b++; // 报错
c++;
d++;
};
cout << a << " " << b << " " << c << " " << d << endl;
cout << "*********************" << endl;
// 混合捕捉2
cout << 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),这个码在编程中经常被使用,这个码可以保证我们的类名唯一性。