👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨
目录
- 一、仿函数的使用
- 二、lambda表达式的语法
- 三、lambda表达式中捕捉列表的玩法
- 四、lambda表达式的底层原理
- 五、总结
一、仿函数的使用
假设需要对vector
容器里的数据进行排序,那么可以通过算法库的 sort
函数进行排序,至于结果为升序还是降序,可以通过 仿函数( 类+operator()
重载) 控制。
struct Great // > - 降序
{
bool operator()(int x, int y)
{
return x > y;
}
};
struct Less // < - 升序
{
bool operator()(int x, int y)
{
return x < y;
}
};
int main()
{
vector<int> v{1, 3, 5, 9, 7, 0, 4, 2, 6, 8};
// Less() - 仿函数的匿名对象
sort(v.begin(), v.end(), Less()); // 升序
cout << "升序:";
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
// Great() - 仿函数的匿名对象
sort(v.begin(), v.end(), Great()); // 降序
cout << "降序:";
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
return 0;
}
【程序结果】
这看起来仿函数用起来也非常的香啊~。但如果某些程序员非常不注重代码风格,是这样写代码的:
struct func1 // > - 降序
{
bool operator()(int x, int y)
{
return x > y;
}
};
struct func2 // < - 升序
{
bool operator()(int x, int y)
{
return x < y;
}
};
sort(v.begin(), v.end(), func1());
sort(v.begin(), v.end(), func2());
这样写是不是非常的恶心,这样命名风格就算了,旁边还不给个注释,这不是欠揍嘛hh;而且万一仿函数在别的文件里,那么我们还要去找。
因此C++11
支持了一个新语法:lambda
表达式。它可以快速构建局部的匿名函数对象,作为函数中的参数。
接下来大家先见见猪跑,至于怎么使用lambda
表达式,后面会介绍
int main()
{
vector<int> v{1, 3, 5, 9, 7, 0, 4, 2, 6, 8};
sort(v.begin(), v.end(), [](int x, int y)->bool {return x < y; }); // 升序
cout << "升序:";
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
// Great() - 仿函数的匿名对象
sort(v.begin(), v.end(), [](int x, int y)->bool {return x > y; }); // 降序
cout << "降序:";
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
return 0;
}
【程序结果】
由此可见,lambda
表达式相对于传统的仿函数来说,更加灵活和简洁。
二、lambda表达式的语法
书写格式:[ ]( ) mutable ->returntype { }
-
[ ]
捕捉列表。该列表总是出现在lambda
函数的开始位置,编译器根据[]
来判断接下来的代码是否为lambda
函数。捕捉列表能够捕捉上下文中的变量供lambda
函数使用。 -
( )
参数列表。与普通函数的参数列表一个意思。如果没有参数,那么可以连通()
一起省略。 -
mutable
关键字。mutable
可以取消捕捉列表中参数的常量属性。注意:使用该修饰符时,即使参数为空,参数列表的()
不可省略。(但是基本不常用) -
->returntype
返回值类型。这里的返回值类型几乎可以不用写。 -
{ }
函数体。和普通函数一样,当然支持多语句。但是不能函数体内不能使用局部的变量及函数,如果想使用,需要配合捕捉列表。
这里再次声明:
-
lambda
表达式 构建出的是一个 局部的匿名函数对象,匿名函数对象也可以调用,不过要在创建后立马调用,因为匿名对象生命周期只有一行。 -
如果需要显式调用,可以使用
auto
推导 匿名函数对象 的类型,然后将创建出来的 匿名函数对象 赋给一个 有名函数对象。
三、lambda表达式中捕捉列表的玩法
-
捕获列表说明:捕捉列表描述了上下文中那些数据可以被
lambda
使用,以及使用的方式传值还是传引用。[val]
:表示值传递捕捉变量val
[&val]
:表示引用传递捕捉变量val
(修改变量)
比如,使用
lambda
表达式交换局部变量a
和b
这是因为 捕捉列表 中的参数是一个传值捕捉,而捕捉列表 中的参数默认具有 常量属性,不能直接修改,但可以添加
mutable
关键字 取消常性
但输出结果发现两个值并没有修改。这里其实就类似于普通函数的值传递,局部变量的
a
和b
只是拷贝给捕捉列表参数,并没有建立联系,因此,这里的捕捉列表需要用引用传递的方式
[&]
:表示引用传递捕捉所有在当前作用域中的所有变量
需要注意的是:
lambda
函数体内不允许使用当前作用域中的变量,除非在捕捉列表声明,否则会报错。这里有两个方法,要么在捕捉列表捕捉,要么将捕捉列表写成
[&]
,表示引用传递捕捉所有在当前作用域中的所有变量。[=]
:表示值传递捕捉所有在当前作用域中的所有变量。
注意:
-
捕捉列表 的使用非常灵活,比如
[&, x]
表示只有x
使用 传值捕捉,其他变量使用 引用捕捉;[=, &str]
表示str
使用 引用捕捉,其他变量使用 传值捕捉。 -
捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:
[=, a]
:捕捉a
重复。 -
lambda
表达式之间不能相互赋值。因为它们之间的类型是不一样的!至于为什么呢?后面讲底层就知道了~
四、lambda表达式的底层原理
其实lambda
表达式用起来非常香,这是因为它的底层是仿函数;就像范围for
一样,看似是简短的几行代码,底层确实一个迭代器。
我们可以使用以下代码样例,通过查看反汇编:
struct func
{
void operator()(int x, int y)
{
cout << "operator()(int x, int y)" << endl;
}
};
int main()
{
int a = 3;
int b = 3;
// 仿函数对象实例化
func f;
f(a, b);
// lambda表达式
auto e = [=](int x, int y) {
cout << "[=](int x, int y)" << endl;
};
e(a, b);
return 0;
a, b);
return 0;
}
【反汇编】
实际在底层编译器对于lambda
表达式的处理方式,完全就是按照仿函数的方式处理的,即:如
果定义了一个lambda
表达式,编译器会自动生成一个类,在该类中重载了operator()
,而捕捉列表就相当于成员变量。
并且这个类被命名为:lambda_uuid
uuid
是 通用唯一标识码,可以生成一个重复率极低的辨识信息,避免类名冲突,这也意味着即便是两个功能完全一样的lambda
表达式,也无法进行赋值,因为lambda_uuid
肯定不一样
所以在编译器看来,lambda
表达式 本质上就是一个 仿函数。
五、总结
当涉及到C++
的lambda
表达式时,可以得出以下总结:
-
匿名函数:
lambda
表达式允许在需要函数对象的地方快速定义匿名函数,无需显式命名,可直接内联使用。 -
捕获外部变量:
lambda
表达式能够捕获其作用域内的变量,可以按值或按引用捕获,使得在算法和回调函数中处理外部变量更加方便。 -
简洁性:
lambda
表达式使代码更加紧凑和简洁,尤其在需要传递简单的函数对象时,可以省去冗余的代码。 -
可读性: 使用
lambda
表达式可以将算法和行为直接嵌入到使用它们的地方,使代码更具可读性和易于理解。 -
函数式编程: 引入
lambda
表达式后,C++
增强了函数式编程的能力,使得处理一些函数式编程的场景更加便利。
需要注意的是,lambda
表达式并不是完全取代传统的仿函数,而是在某些情况下更加方便和实用。例如,在需要长期复用、需要命名或需要在多个地方多次使用的情况下,传统的仿函数仍然有其价值。