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,并在实际开发中更高效地运用这一特性。如果你有任何问题或想法,欢迎在评论区交流探讨!

相关推荐
戴国进7 分钟前
全面讲解python的uiautomation包
开发语言·python
橘猫云计算机设计29 分钟前
基于Java的班级事务管理系统(源码+lw+部署文档+讲解),源码可白嫖!
java·开发语言·数据库·spring boot·微信小程序·小程序·毕业设计
叱咤少帅(少帅)39 分钟前
Go环境相关理解
linux·开发语言·golang
熬了夜的程序员42 分钟前
Go 语言封装邮件发送功能
开发语言·后端·golang·log4j
士别三日&&当刮目相看1 小时前
JAVA学习*String类
java·开发语言·学习
王嘉俊9251 小时前
ReentranLock手写
java·开发语言·javase
my_realmy1 小时前
JAVA 单调栈习题解析
java·开发语言
海晨忆1 小时前
JS—ES5与ES6:2分钟掌握ES5与ES6的区别
开发语言·javascript·es6·es5与es6的区别
高飞的Leo2 小时前
工厂方法模式
java·开发语言·工厂方法模式
菜鸡中的奋斗鸡→挣扎鸡2 小时前
c++ count方法
开发语言·c++