🌈欢迎来到C++专栏~~ lambda 表达式
- (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是Scort
- 目前状态:大三非科班啃C++中
- 🌍博客主页:张小姐的猫~江湖背景
- 快上车🚘,握好方向盘跟我有一起打天下嘞!
- 送给自己的一句鸡汤🤔:
- 🔥真正的大师永远怀着一颗学徒的心
- 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏
- 🎉🎉欢迎持续关注!
一. 概念
自 C++11 开始,C++ 有三种方式可以像函数使用的对象 / 类型:
函数指针
仿函数
Lambda 表达式
此处要注意:greater后面的括号是什么时候要加上?(容易混淆)
- 首先我们要清楚,传的是对象还是类型:传的是对象就要加(),如果是类型就不用
言归正传,lambda
表达式本质上就是一个匿名函数
这里举个例子:
struct Items
{
string _name; //名字
double _price; //价格
int _num; //数量
};
如果要对若干对象分别按照价格和数量进行升序、降序排序
可以使用 sort 函数,但由于这里待排序的元素为自定义类,如果想按照我们的数据进行排序,只能通过仿函数来实现了,那岂不是要实现6个仿函数?
有点麻烦了
struct ***parePriceLess//价格降序
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._price < g2._price;
}
};
struct ***parePriceGreater//价格升序
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._price > g2._price;
}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 300 }, { "香蕉", 3.3, 100 }, { "橙子", 2.2, 1000 }, { "菠萝", 1.5, 1 } };
sort(v.begin(), v.end(), ***parePriceLess()); //价格升序
sort(v.begin(), v.end(), ***parePriceGreater()); //价格降序
return 0;
}
为此lambda
表达式就横空出世了
二. 语法
lambda 表达式定义:
[capture-list] (parameters) mutable -> return-type { statement }
表达式各部分说明:
-
[capture-list]
: 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用 -
(parameters)
:参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略(无参可以省略) -
mutable
:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量
性。使用该修饰符时,参数列表不可省略(即使参数为空) -
->returntype
:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导 -
{statement}
:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量
除了捕获列表,Lambda表达式的其它地方其实和普通的函数基本一样,lambda 参数列表和返回值类型都是可有可无的,但捕捉列表和函数体是不可省略的,因此最简单的lambda函数如下:
int main()
{
[]{}; //最简单的lambda表达式
return 0;
}
再举个函数相加的例子:
int main()
{
//两个函数相加
auto add1 = [] (int a, int b)->int{return a + b; };
cout << add1(1, 2) << endl;
//省略返回值
auto add2 = [](int a, int b){return a + b; };
cout << add1(1, 2) << endl;
return 0;
}
如果我们要不传参数,该怎么样实现呢?
三. 捕获方式
Lambda 表达式最基本的两种捕获方式是:按值捕获和按引用捕获
-
[var]
:值传递捕捉变量var -
[=]
:值传递捕获所有父作用域中的变量(成员函数包括this指针) -
[&var]
:引用传递捕捉变量var -
[&]
:引用传递捕捉所有父作用域中的变量(成员函数包括this指针)
注意:
- 1️⃣父作用域要包含lambda函数语句(一般指的是当前所在的函数(栈帧))
- 2️⃣语法上捕捉可由多个捕捉项组成,并以逗号分割(就是可以混合着来)
//混合捕捉:a是引用捕捉,其他都是传值捕捉
auto f1 = [=, &a]() {
cout << a << b << c << d << e << endl;
};
f1();
- 3️⃣捕捉列表不允许变量重复传递,否则就会导致编译错误
- 4️⃣在块作用域以外的lambda函数捕捉列表必须为空
- 5️⃣在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都 会导致编译报错。
来个例题:此处的f
可以++吗?
int f = 0;
int main()
{
int a, b, c, d, e;
a = b = c = d = e = 1;
//混合捕捉:a是引用捕捉,其他都是传值捕捉
auto f1 = [=, &a]() {
f++;
cout << a << b << c << d << e << endl;
};
f1();
return 0;
}
f为什么可以呢?
- 因为
f
是全局变量不存在于栈帧里,存在于静态区,哪个位置都可以用它!
复习一下:
对象的作用域和存储区域要分清楚:
- 生命周期是和存储的区域有关系
- 作用域(编译器编译,用的地方能否找到) 局部 —> 全局
💥相互赋值
lambda表达式之间不能相互赋值,就算是两个一模一样的也不行
lambda 表达式会被处理为函数对象,该函数对象对应的类名叫做<lambda_uuid>
类名中的uuid
叫做通用唯一识别码,简单来说就是通过算法生成的一串字符串,它具有随机性和不重复性,保证在当前程序中每次生成不同的 uuid,因为 lambda 表达式底层的类名包含 uuid,这就保证了每个 lambda 表达式底层类名都是唯一的!
void (*PF)();
int main()
{
auto f1 = []{cout << "hello world" << endl; };
auto f2 = []{cout << "hello world" << endl; };
f1 = f2; // 编译失败--->提示找不到operator=()
//实例化后的两个lambda类型,类型不一样
auto f3(f2);
f3();
// 可以将lambda表达式赋值给相同类型的函数指针
PF = f2;
PF();
return 0;
}
但是lambda表达式赋值给相同类型的函数指针
就是在我们看来是一样的,但是其底层大有不同!
四. 底层实现
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,就是对()进行了重载
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;
}
对反汇编进行观察:
当创建add对象的时候是构造函数,使用add对象的时候就是会调用 Add 类的 () 运算符重载函数
lambda 表达式同样如此:会调用 <lambda_uuid>
类的构造函数,在使用add2对象时,会调用<lambda_uuid>
类的 ()运算符重载函数
其本质就是:lambda表达式在底层被转换成了仿函数
当我们定义一个lambda表达式后,编译器会自动生成一个类,在该类中对 () 运算符进行重载,实际 lambda 函数体的实现就是这个仿函数 operator() 的实现,在调用 lambda 表达式时,参数列表和捕获列表的参数,最终都传递给了仿函数的 operator()
五. mutable(作用不大)
在实际使用中,比如实现一个交换函数,我们用 lambda 表达式实现:
int main()
{
auto swap2 = [x, y]()
{
int tmp = x;
x = y;
y = tmp;
};
swap2();
return 0;
}
这里我们发现是传值传参!果然编译不通过,因为传值捕获到的变量默认是不可修改的(const):
如果要取消其常量属性,就需要在 lambda 表达式中加上 mutable
像这样:
auto swap2 = [x, y]() mutable //改变的是形参,实参无影响,所以没用
{
int tmp = x;
x = y;
y = tmp;
};
但是捕捉列表是传值捕捉过来的,不影响外面的实参;所以这种方法无法完成交换功能