前言
在现代C++中,Lambda表达式提供了一种简洁而强大的方式来定义匿名函数,使代码更具可读性和灵活性。自C++11引入Lambda以来,它已经成为STL算法、并发编程和回调机制中的重要工具。随着C++14、C++17和C++20的不断演进,Lambda的功能也在不断增强,进一步提升了C++语言的表达能力。
本文将从Lambda的语法入手,分析其捕捉列表的工作原理,探讨Lambda在实际开发中的应用,并深入剖析Lambda背后的实现原理,帮助大家全面掌握这一强大特性。
1.表达式语法介绍
1.lambda 表达式本质是一个匿名函数对象,跟普通函数不同的是它可以定义在函数内部。
lambda 表达式语法使用层而言没有类型,所以一般是用auto或者模板参数定义的对象去接收 lambda 对象。
2. lambda表达式的格式: [capture-list] (parameters)-> return type { function boby }
3.[capture-list] : 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据[]来
判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使
用,捕捉列表可以传值和传引用捕捉,具体细节7.2中我们再细讲。捕捉列表为空也不能省略。
(parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连
同()⼀起省略
•
->return type :返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此
部分可省略。一般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
•
{function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以
使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
简单的lambda表达式实例
cpp
#include <iostream>
using namespace std;
int main() {
auto add=[](int x,int y)->int {return x+y;};
cout<<add(1,2)<<endl;
return 0;
}
1 、捕捉为空也不能省略
2 、参数为空可以省略
3 、返回值可以省略,可以通过返回对象自动推导
4 、函数体不能省略
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
int main() {
auto add = [](int x, int y)->int {return x + y; };
cout << add(1, 2) << endl;
auto print = [] {cout << "hello world" << endl; };
print();
int a = 0, b = 1;
auto swap = [](int& x, int& y) {
int temp = x;
x = y;
y = temp;
};
swap(a, b);
cout << a << " " << b << endl;
return 0;
}

2.捕捉列表分析
- lambda 表达式中默认只能用 lambda 函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉
- 第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。[x,y, &z] 表示x和y值捕捉,z引用捕捉。
cpp
int x = 0;
auto func1 = [] {x++;};
// 捕捉列表必须为空,因为全局变量不⽤捕捉就可以⽤,没有可被捕捉的变量
int main() {
int a = 0, b = 1, c = 2, d = 3;
auto func2 = [a, &b] {
//a++;不可修改
b++;
x++;
int ret = a + b + x;
return ret;
};
cout << func2() << endl;
}
只能用当前局部域的对象和捕捉以及与全局变量,值捕捉不能修改,引用捕捉可以修改
3. **第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写一个=表示隐式值捕捉,在捕捉列表写一个&表示隐式引用捕捉,**这样我们 lambda 表达式中用了那些变量,编译器就会自动捕捉那些变量。
cpp
// 隐式捕捉 隐式引⽤捕捉
// ⽤了哪些变量就捕捉哪些变量
auto func2 = [=] {int ret = a + b + c + d; return ret; };
cout << func2() << endl;
auto func3 = [&] {a++; b++; c++; };
func3();
cout<<"a="<<a<<" b="<<b<<" c="<<c<<" d="<<d << endl;

- 第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[=, &x]表示其他变量隐式值捕捉,x引用捕捉;[&, x, y]表示其他变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第一个元素必须是&或=,并且&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引用捕捉。
cpp
auto func4 = [&, a, b]
{
//a++;
//b++;
c++;
d++;
return a + b + c + d;
};
cout<<func4()<<endl;
cout << a << " " << b << " " << c << " " << d << endl;
// 混合捕捉1
auto func5 = [=, &a, &b]
{
a++;
b++;
/*c++;
d++;*/
return a + b + c + d;
};
cout<<func5()<<endl;
cout << a << " " << b << " " << c << " " << d << endl;
5. lambda 表达式如果在函数局部域中,可以捕捉 lambda 位置之前定义的变量,不能捕捉静态
局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使
用。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
cpp
static int m = 5;
auto func6 = [] {return x + m; };
cout << func6() << endl;
默认情况下, lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,
mutable加在参数列表的后用可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以
修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。
cpp
auto func7 = [=]()mutable
{
a++;
b++;
c++;
d++;
return a + b + c + d;
};

3.lambda的应用
在学习 lambda 表达式之前,我们的使用的可调 用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对会比较麻烦。使用 lambda 去定义可用对象,既简单又方便。
lambda 在很多其他地方用起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等, lambda 的应用还是很广泛的,以后我们会不断接触到。
cpp
struct books {
string name;
int price;
books(const string& name, int price)
: name(name), price(price)
{}
};
struct compareLess {
bool operator()(const books& a, const books& b) {
return a.price < b.price;
}
};
struct compareGreater {
bool operator()(const books& a, const books& b) {
return a.price > b.price;
}
};
int main() {
vector<books> books = { {"西游记",45},{"红楼梦",54},{"三国演义",42}};
sort(books.begin(), books.end(), compareGreater());
sort(books.begin(), books.end(), compareLess());
return 0;
}
对于自定义对象的排序,我们采用了仿函数的形式,定义了相关的类,如果有很多种比较情况,就要写很多,这时我们可以采用仿函数的形式。
cpp
sort(books.begin(), books.end(), [](const Books& a, const Books& b) {
return a.price < b.price;
});
sort(books.begin(), books.end(), [](const Books& a, const Books& b) {
return a.price > b.price;
});
sort(books.begin(), books.end(), [](const Books& a, const Books& b) {
return a.id < b.id;
});
sort(books.begin(), books.end(), [](const Books& a, const Books& b) {
return a.id > b.id;
});
4.lambda的原理
lambda 的原理和范围for很像,编译后从汇编指令层的角度看,压根就没有 lambda 和范围for
这样的东西。范围for底层是迭代器,而lambda底层是仿函数对象,也就说我们写了一个
lambda 以后,编译器会生成一个对应的仿函数的类。
cpp
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.49;
// lambda
auto r2 = [rate](double money, int year) {
return money * rate * year;
};
// 函数对象
Rate r1(rate);
r1(10000, 2);
r2(10000, 2);
auto func1 = [] {
cout << "hello world" << endl;
};
func1();
return 0;
}
仿函数的类名是编译按⼀定规则生成的,保证不同的 lambda 生成的类名不同,lambda参数/返
回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda 的捕捉列表本质是生成
的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传那些对象。
上面的原理,我们可以透过汇编层了解一下,下面第二段汇编层代码印证了上面的原理。
// 汇编层可以看到 r2 lambda 对象调用本质还是调用 operator() ,类型是 lambda_1, 这个类型名
// 的规则是编译器自己定制的,保证不同的 lambda 不冲突
结束语
Lambda表达式的引入极大地提升了C++的编程体验,使得函数式编程风格更易于在C++中实现。无论是简化代码、提升可读性,还是提高运行时效率,Lambda都扮演着重要的角色。理解其语法、捕捉列表以及底层原理,不仅能帮助开发者更好地使用Lambda,还能在需要时优化其性能。
希望本文能帮助你深入理解C++ Lambda,并在实际开发中更高效地运用这一特性。如果你有任何问题或想法,欢迎在评论区交流探讨!