Lambda表达式
- 1.lambda表达式语法
- 2.捕获列表说明
- 3.lambda表达式实现原理
- 4.具体案例
- 5.总结
1.lambda表达式语法
- ambda表达式的语法非常简单,具体定义如下:
[ captures ] ( params ) specifiers exception -> ret { body }
先不用急于解读这个定义,我们可以结合lambda表达式的例子来读
懂它的语法:
#include <iostream>
int main()
{
int x = 5;
auto foo = [x](int y)->int { return x * y; };
std::cout << foo(8) << std::endl;
}
- 在这个例子中,[x](int y)->int { return x * y; }是一个标准的lambda表
达式,对应到lambda表达式的语法。
- [ captures ] —— 捕获列表,它可以捕获当前函数作用域的零个或多个变量,变量之间用逗号分隔。在对应的例子中,[x]是一个捕获列表,不过它只捕获了当前函数作用域的一个变量x,在捕获了变量之后,我们可以在lambda表达式函数体内使用这个变量,比如return x *y。另外,捕获列表的捕获方式有两种:按值捕获和引用捕获,下文会详细介绍。
- ( params ) —— 可选参数列表,语法和普通函数的参数列表一样,在不需要参数的时候可以忽略参数列表。对应例子中的(int y)。specifiers ——可选限定符,C++11中可以用mutable,它允许我们在lambda表达式函数体内改变按值捕获的变量,或者调用非const的成员函数。上面的例子中没有使用说明符。
- exception —— 可选异常说明符,我们可以使用noexcept来指明lambda是否会抛出异常。对应的例子中没有使用异常说明符。ret —— 可选返回值类型,不同于普通函数,lambda表达式使用返回类型后置的语法来表示返回类型,如果没有返回值(void类型),可以忽略包括->在内的整个部分另外,我们也可以在有返回值的情况下不指定返回类型,这时编译器会为我们推导出一个返回类型。对应到上面的例子是->int。{ body } —— lambda表达式的函数体,这个部分和普通函数的函数体一样。对应例子中的{ return x * y; }。
2.捕获列表说明
- 捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
- 注意:
- 父作用域指包含lambda函数的语句块
- 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
- 捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复d. 在块作用域以外lambda函数捕捉列表必须为空。
- 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
- lambda表达式之间不能相互赋值,即使看起来类型相同(因为lambda表达式底层是 :每一个lambda都会被底层转换成一个仿函数, 仿函数的名称=lambda表达式+uuid,uuid是独立的,所有每个lambda表达式再构造的时候都是独立且唯一的,那么在表达式相互赋值的时候编译器就会判断类型不同,无法进行相互赋值。)
- 代码测试
void test()//lambda介绍
{
int a = 10;
int b = 5;
//完整的lambda表达式
auto func1 = [](int a, int b)->int{return a * b; };
cout << func1(a, b) << endl;
//缺省参数和返回值类型
auto func2 = [=]() {return a + b; };
cout << func2() << endl;
//捕捉值
//auto func3 = [=]() {return a = a * b; }; //编译失败 a是常量 左值 不可修改
auto func3 = [&a,b]() {return a = a * b; };//a传引用 b是捕捉值(相当于复制的) 编译通过
//a的值被修改
cout << "a="<< func3() << endl;
}
3.lambda表达式实现原理
- 先看一段代码
#include <iostream>
class Bar
{
public:
Bar(int x, int y) : x_(x), y_(y) {}
int operator () ()
{
return x_ * y_;
}
private:
int x_;
int y_;
};
int main()
{
int x = 5, y = 8;
auto foo = [x, y] { return x * y; };
Bar bar(x, y);
std::cout << "foo() = " << foo() << std::endl;
std::cout << "bar() = " << bar() << std::endl;
}
在上面的代码中,foo是一个lambda表达式,而bar是一个函数对
象。它们都能在初始化的时候获取main函数中变量x和y的值,并在调用
之后返回相同的结果。这两者比较明显的区别如下。
1.使用lambda表达式不需要我们去显式定义一个类,这一点在快
速实现功能上有较大的优势。
2.使用函数对象可以在初始化的时候有更加丰富的操作,例如Bar
bar(x+y, x * y),而这个操作在C++11标准的lambda表达式中是不允许
的。另外,在Bar初始化对象的时候使用全局或者静态局部变量也是没
有问题的。
这样看来在C++11标准中,lambda表达式的优势在于书写简单方便
且易于维护,而函数对象的优势在于使用更加灵活不受限制,但总的来
说它们非常相似。而实际上这也正是lambda表达式的实现原理。
lambda表达式在编译期会由编译器自动生成一个闭包类,在运行时
由这个闭包类产生一个对象,我们称它为闭包。在C++中,所谓的闭包
可以简单地理解为一个匿名且可以包含定义时作用域上下文的函数对
象。现在让我们抛开这些概念,观察lambda表达式究竟是什么样子的。
首先,定义一个简单的lambda表达式:
#include <iostream>
int main()
{
int x = 5, y = 8;
auto foo = [=] { return x * y; };
int z = foo();
}
接着,我们用GCC输出其GIMPLE的中间代码:
int main ()
{
int D.39253;
{
int x;
int y;
struct __lambda0 foo;
typedef struct __lambda0 __lambda0;
int z;
try
{
x = 5;
y = 8;
foo.__x = x;
foo.__y = y;
z = main()::<lambda()>::operator() (&foo);
}
finally
{
foo = {CLOBBER};
}
}
D.39253 = 0;
return D.39253;
}
main()::<lambda()>::operator() (const struct __lambda0 * const __closure)
{
int D.39255;
const int x [value-expr: __closure->__x];
const int y [value-expr: __closure->__y];
_1 = __closure->__x;
_2 = __closure->__y;
D.39255 = _1 * _2;
return D.39255;
}
从上面的中间代码可以看出lambda表达式的类型名为__lambda0,
通过这个类型实例化了对象foo,然后在函数内对foo对象的成员__x和
__y进行赋值,最后通过自定义的()运算符对表达式执行计算并将结果赋
值给变量z。在这个过程中,__lambda0是一个拥有operator()自定义运算
符的结构体,这也正是函数对象类型的特性。所以,在某种程度上来
说,lambda表达式是C++11给我们提供的一块语法糖而已,lambda表达
式的功能完全能够手动实现,而且如果实现合理,代码在运行效率上也
不会有差距,只不过实用lambda表达式让代码编写更加轻松了。
4.具体案例
在下面我会用代码演示lambda表达式在比较场景的使用:
在网购系统中,如果要针对不同商品的属性进行排序,C+11出现之前我们都是用实现仿函数的+调用sort方式来比较,总统来说比较复杂,可读性也不高,lambda表达式的出现就大大改进了书写的方式,lambda表达式实际上就是一块语法糖,提高代码的书写效率和可阅读性。
- 代码:
#define _CRT_SECURE_NO_WARNINGS
using namespace std;
#include<iostream>
#include <algorithm>
#include<vector>
//lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
//[capture-list] -----捕捉列表 [=]:捕捉所有值、[&]:捕捉所有变量的引用、[&,a,b]:a、b为捕捉值,其余为引用
//(parameters) ----参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
//mutable----默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
//return-type---返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分
//可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
//{ statement }---函数体
struct Goods
{
string name;
double price;//价格
int Id;//编号
};
struct Priceless
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1.price < g2.price;
}
};
struct IDLess
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1.Id < g2.Id;
}
};
void compare()
{
//c++98
cout << "C+98方式实现:" << endl;
cout << endl;
std::vector<Goods> v;
v = { Goods{"苹果", 55.55, 1}, {"西瓜", 26.23, 2}, {"香蕉", 66.66, 3} };
cout << "价格升序:" << endl;
std::sort(v.begin(), v.end(), Priceless());
for (const auto& e : v) {
std::cout << "Name: " << e.name << ", Price: " << e.price << ", ID: " << e.Id << std::endl;
}
cout << endl;
cout << "ID升序:" << endl;
std::sort(v.begin(), v.end(), IDLess());
for (const auto& e : v) {
std::cout << "Name: " << e.name << ", Price: " << e.price << ", ID: " << e.Id << std::endl;
}
//C+11 lambda表达式
cout << "C+11lambda表达式实现:" << endl;
cout << "价格升序:" << endl;
std::sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1.price < g2.price; });
for (const auto& e : v) {
std::cout << "Name: " << e.name << ", Price: " << e.price << ", ID: " << e.Id << std::endl;
}
cout << endl;
cout << "ID升序:" << endl;
std::sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1.Id < g2.Id; });
for (const auto& e : v) {
std::cout << "Name: " << e.name << ", Price: " << e.price << ", ID: " << e.Id << std::endl;
}
cout << endl;
}
int main()
{
//test();
compare();
return 0;
}
- 运行结果:
5.总结
总的来说lambda表达式不但容易使用,而且原理也容易理解。它很好地解决了过去C++中无法直接编写内嵌函数的尴尬。虽然在GCC中提供了一个叫作nest function的C语言扩展,这个扩展允许我们在函数内部编写内嵌函数,但这个特性一直没有被纳入标准当中。当然我们也并不用为此遗憾因为现在提供的lambda表达式无论在语法简易程度上,还是用途广泛程度上都要优于nest function。合理地使用lambda表达式,可以让代码更加短小精悍的同时也具有良好的可读性。