lambda表达式

发布于:2025-05-12 ⋅ 阅读:(63) ⋅ 点赞:(0)

引入:lambda表达式的意义是什么?

Lambda表达式是C++11引入的匿名函数对象,旨在简化代码、提升开发效率,尤其在需要短小临时函数的场景中替代传统仿函数。

一:繁琐的场景

#include<iostream>
#include<algorithm>
#include<vector>

using namespace std;

struct Goods
{
	string _name;  // 名字
	double _price; // 价格
	int _evaluate; // 评价

	Goods(const char* str, double price, int evaluate)
		: _name(str)
		, _price(price)
		, _evaluate(evaluate)
	{}
};

struct ComparePriceLess
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price < gr._price;
	}
};

struct ComparePriceGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price > gr._price;
	}
};

int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3.1, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };

	sort(v.begin(), v.end(), ComparePriceLess());
	for (auto& e : v)
	{
		cout << e._name << " " << e._price << " " << e._evaluate << endl;

	}

	cout << "------------------" << endl;
	sort(v.begin(), v.end(), ComparePriceGreater());
	for (auto& e : v)
	{
		cout << e._name << " " << e._price << " " << e._evaluate << endl;

	}
}

运行结果:

解释:想要比较什么,就自己去写一个仿函数

随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,
都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,
这些都给编程者带来了极大的不便因此,在C++11语法中出现了Lambda表达式。

二:lambda表达式的格式

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement}
1. lambda表达式各部分说明
A:[capture-list] : 捕捉列表 ,该列表总是出现在 lambda 函数的开始位置, 编译器根据 []
判断接下来的代码是否为 lambda 函数 捕捉列表能够捕捉上下文中的变量供 lambda
函数使用
B:(parameters):参数列表 。与 普通函数的参数列表一致 ,如果不需要参数传递,则可以
连同 () 一起省略
C:mutable :默认情况下, lambda 函数总是一个 const 函数, mutable 可以取消其常量
性。使用该修饰符时,参数列表不可省略 ( 即使参数为空 )
D:->returntype :返回值类型 。用 追踪返回类型形式声明函数的返回值类型 ,没有返回
值时此部分可省略。 返回值类型明确情况下,也可省略,由编译器对返回类型进行推
E:{statement}:函数体 。在该函数体内,除了可以使用其参数外,还可以使用所有捕获
到的变量。
注意:
lambda 函数定义中, 参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为
。因此 C++11 最简单的 lambda 函数为: []{} ; lambda 函数不能做任何事情。
看这些规则,总会觉得很繁杂,举个例子就好了
int main()
{
	// 局部的匿名函数对象
	auto add = [](int a, int b)->int {return a + b; };
	
	cout << add(1, 2) << endl;

	return 0;
}
运行结果:
甚至可以写成:
//可以去掉箭头->
auto add = [](int a, int b) {return a + b; };

理解:[ ] + () + {}

[ ]作用后面会讲

()里面写参数,就像函数的参数部分一样

{}里面写函数体的内容

多举几个lambda例子:

例子1:

int main()
{

	auto swap1 = [](int& a, int& b)->void 
    {
		int tmp = a;
		a = b;
		b = tmp;
	};

	int x = 1, y = 2;
	swap1(x, y);

    cout << x << " " << y << endl;


	return 0;
}

运行结果:

例子2:

int main()
{
	auto func1 = []
	{
		cout << "hello world" << endl;
	};

	func1();

	return 0;
}

运行结果:

解释:没参数的时候 ()都不用写了

总结:通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调 用,如果想要直接调用,可借助auto将其赋值给一个变量。

三:用lambda改造繁杂场景

用lambda改造一中的繁琐场景后:

int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };

	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 使用 ,以及 使用的方式传值还是传引用
[var]: 表示值传递方式捕捉变量 var
[=]: 表示值传递方式捕获所有父作用域中的变量 ( 包括 this)
[&var]: 表示引用传递捕捉变量 var
[&]: 表示引用传递捕捉所有父作用域中的变量 ( 包括 this)
[this]: 表示值传递方式捕捉当前的 this 指针
注意:
a. 父作用域指包含 lambda 函数的语句块
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割
c. 捕捉列表不允许变量重复传递,否则就会导致编译错误
d. 在块作用域以外的 lambda 函数捕捉列表必须为空
e. 在块作用域中的 lambda 函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者
非局部变量都会导致编译报错。
f. lambda 表达式之间不能相互赋值 ,即使看起来类型相同
哎,看起来真的复杂,直接举例子吧,理解了例子就够了
我们之前用lambda写swap函数是这样写的:
int main()
{

	auto swap1 = [](int& a, int& b)->void 
    {
		int tmp = a;
		a = b;
		b = tmp;
	};

	int x = 1, y = 2;
	swap1(x, y);

    cout << x << " " << y << endl;


	return 0;
}

我们还可以通过捕获列表 [&] 隐式捕获外部变量 a 和 b(引用捕获):

①:swap函数的引用捕获

int main()
{
	int a = 1, b = 2;
	auto Swap = [&]
	{
		int tmp = a;
		a = b;
		b = tmp;
	};
	Swap(); //交换a和b
    cout<<a<<b<<endl;
	return 0;
}
运行结果:

②:swap的传值方式捕获

如果以传值方式进行捕捉,那么首先编译不会通过,因为传值捕获到的变量默认是不可修改的,如果要取消其常量性,就需要在lambda表达式中加上mutable,并且此时参数列表不可省略。比如:
int main()
{
	int a = 1, b = 2;
	auto Swap = [a, b]()mutable
	{
		int tmp = a;
		a = b;
		b = tmp;
	};
	Swap(); //交换a和b?

    cout<<a<<""<<b<<endl;
	return 0;
}

运行结果:

发现没变,因为这里是传值捕捉,lambda函数中对a和b的修改不会影响外面的a、b变量,与函数的传值传参是一个道理,因此这种方法无法完成两个数的交换。

③:捕捉列表的风险

a:引用捕获的风险

int main() {
    int a = 10, b = 20;
    int c = 30, d = 40; // 其他变量

    auto Swap = [&] { // [&] 会隐式捕获所有可见变量(a、b、c、d)
        int tmp = a;
        a = b;
        b = tmp;
        // 这里也可以访问 c 和 d(但实际未使用)
    };
    Swap();
}
  • 所有局部变量(a、b、c、d)都会被引用捕获,即使 lambda 内部未使用它们。

  • 潜在风险:如果 lambda 被传递到其他作用域(如异步任务),可能导致悬垂引用(访问已销毁的变量)。

b:传值捕获的风险

auto Swap = [=] { // 错误!a 和 b 被值捕获,无法修改(默认值捕获是 const)
    a = b; // 编译错误:a 是只读的
};

正确写法:显式指定捕获的变量(推荐做法)

auto Swap = [&a, &b] { // 只捕获 a 和 b,其他变量(c、d)不受影响
    int tmp = a;
    a = b;
    b = tmp;
};

总结:

关键结论

  1. [&] 和 [=] 会捕获所有可见变量(即使未使用),可能引发性能或安全问题。

  2. 推荐显式列出需要捕获的变量(如 [&a, &b]),避免隐式捕获的副作用。

  3. 值捕获 ([a, b]) 无法修改原变量,除非用 mutable(但通常引用捕获更符合交换操作的需求)。

五:汇编下的lambda

lambda看起来这么的神奇,但其实底层和仿函数一模一样!
就好像范围for好牛,其实本质就是迭代器......
看下面例子的汇编:
class Add
{
public:
	Add(int base)
		:_base(base)
	{}
	int operator()(int num)
	{
		return _base + num;
	}
private:
	int _base;
};
int main()
{
	int base = 1;

	//函数对象
	Add add1(base);
	add1(1000);

	//lambda表达式
	auto add2 = [base](int num)->int
	{
		return base + num;
	};
	add2(1000);
	return 0;
}

调试代码并转到反汇编,可以看到:

  • 在创建函数对象add1时,会调用Add类的构造函数。
  • 在使用函数对象add1时,会调用Add类的()运算符重载函数。

如下图:

观察lambda表达式时,也能看到类似的代码:

  • 借助auto将lambda表达式赋值给add2对象时,会调用<lambda_uuid>类的构造函数。
  • 在使用add2对象时,会调用<lambda_uuid>类的()运算符重载函数。

如下图:

本质就是因为lambda表达式在底层被转换成了仿函数。

  • 当我们定义一个lambda表达式后,编译器会自动生成一个类,在该类中对()运算符进行重载,实际lambda函数体的实现就是这个仿函数的operator()的实现。
  • 在调用lambda表达式时,参数列表和捕获列表的参数,最终都传递给了仿函数的operator()

lambda表达式和范围for是类似的,它们在语法层面上看起来都很神奇,但实际范围for底层就是通过迭代器实现的,lambda表达式底层的处理方式和函数对象是一样的。

所以,由上面也可以佐证一个结论:

lambda表达式之间不能相互赋值

lambda表达式之间不能相互赋值,就算是两个一模一样的lambda表达式。

解释:

a:因为lambda表达式底层的处理方式和仿函数是一样的,在VS下,lambda表达式在底层会被处理为函数对象,该函数对象对应的类名叫做<lambda_uuid>。
b:类名中的uuid叫做通用唯一识别码(Universally Unique Identifier),简单来说,uuid就是通过算法生成一串字符串,保证在当前程序当中每次生成的uuid都不会重复。
c:lambda表达式底层的类名包含uuid,这样就能保证每个lambda表达式底层类名都是唯一的。

例子(一模一样的lambda表达式,它们的类型都是不同):

int main()
{
	int a = 10, b = 20;
	auto Swap1 = [](int& x, int& y)->void
	{
		int tmp = x;
		x = y;
		y = tmp;
	};
	auto Swap2 = [](int& x, int& y)->void
	{
		int tmp = x;
		x = y;
		y = tmp;
	};
	cout << typeid(Swap1).name() << endl; //class <lambda_797a0f7342ee38a60521450c0863d41f>
	cout << typeid(Swap2).name() << endl; //class <lambda_f7574cd5b805c37a13a7dc214d824b1f>
	return 0;
}

可以看到,就算是两个一模一样的lambda表达式,它们的类型都是不同的。

说明: 编译器只需要保证每个lambda表达式底层对应类的类名不同即可,并不是每个编译器都会将lambda表达式底层对应类的类名处理成<lambda_uuid>,这里只是以VS为例。

六:lambda的特性及其影响场景

①:lambda的特性

1:禁止默认构造

  • Lambda 表达式对应的类型没有默认构造函数,因此不能直接声明未初始化的 Lambda 对象。

  • 示例:

auto lambda = []{ return 42; };
decltype(lambda) l;  // 错误:无法默认构造

2:允许拷贝构造

  • Lambda 对应的类型允许拷贝构造,允许通过已有的 Lambda 对象初始化新对象。

  • 示例:

auto lambda1 = []{ return 42; };
auto lambda2 = lambda1;  // 正确:调用拷贝构造

lambda叫作闭包类型

而由于lambda有这两个特性,在priority_queue的场景中,写法和仿函数会有所差距

②:特性影响的场景

场景:将 Lambda 作为自定义比较器传递给 STL 容器priority_queue 

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int year, int month, int day)" << endl;
	}

	Date(const Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	{
		cout << "Date(const Date& d)" << endl;
	}


    // 1在 Date 类中重载 < 运算符
    bool operator<(const Date& d) const {
    if (_year != d._year) return _year < d._year;
    if (_month != d._month) return _month < d._month;
    return _day < d._day;
}

private:
	int _year = 1;
	int _month = 1;
	int _day = 1;
};
#include<queue>

int main()
{
	
    auto DateLess = [](const Date* p1, const Date* p2) 
    {
    return *p1 < *p2;  // 比较对象内容而非指针地址
    };

	priority_queue<Date*, vector<Date*>, decltype(DateLess)> p1(DateLess);

	return 0;
}

代码意义:演示如何将 Lambda 作为自定义比较器传递给 STL 容器(需显式指定类型并传递实例

重点就在这一行代码:

priority_queue<Date*, vector<Date*>, decltype(DateLess)>  p1(DateLess);

理解点1:第三个参数:

decltype(DateLess)

如果是普通的仿函数的话,这里直接传这个仿函数类的类名就好了,但是我们用的lambda表达式,由于 Lambda 的类型是匿名且编译器生成的,不知道名字啊,所以只能decltype(DateLess)了

理解点2:

 p1(DateLess)

Q:为什么需要显式传递 DateLess 对象,而不仅仅是声明类型

A: priority_queue 内部实际需要一个 该类型的实例对象 来调用比较逻辑。

        所以如果你是仿函数,这里只用p1即可,因为仿函数可以默认构造,priority_queue 内部会自己创建一个实例;而lambda没有默认构造,所以priority_queue 内部无法通过参数3这个类型来实例化出对象,所以我们只能必须显示的调用拷贝构造,DataLess就是lambda表达式的一个对象,其通过拷贝构造给p1

一段又有趣的话:


网站公告

今日签到

点亮在社区的每一天
去签到