C++11之深度理解lambda表达式

前言

在现代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.捕捉列表分析

  1. lambda 表达式中默认只能用 lambda 函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉
  2. 第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。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;
  1. 第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉=, \&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,并在实际开发中更高效地运用这一特性。如果你有任何问题或想法,欢迎在评论区交流探讨!

相关推荐
YY&DS6 分钟前
Qt Designer 自定义控件已提升后,如何修改提升类
开发语言·qt
Brilliantwxx8 分钟前
【C++】 深入理解红黑树:实现与原理全解
数据结构·c++·笔记·算法·青少年编程·红黑树
右耳朵猫AI16 分钟前
Rust技术周刊 2026年第19周
开发语言·后端·rust
Leweslyh26 分钟前
基于 Confucius 架构的无人集群网络控制原语解析
开发语言·网络·php
月落归舟39 分钟前
Java线程小记
java·开发语言
摇滚侠1 小时前
01 基础语法 JavaScript 入门到精通全套教程
开发语言·javascript·ecmascript
sleven fung1 小时前
Milvus 向量数据库
开发语言·数据库·python·langchain·milvus
大大杰哥1 小时前
Java 日志框架详解:SLF4J + Logback 从入门到实战
java·开发语言·logback
ylscode1 小时前
黑客利用 GHOSTYNETWORKS 和 OMEGATECH 托管 JS 恶意软件基础设施
开发语言·安全·php·安全威胁分析
莫等闲-1 小时前
leetcode42. 接雨水 leetcode84.柱状图中最大的矩形
数据结构·c++·算法·leetcode