C++11:lambda
上篇文章:C++11:新的类功能
目录
1.lambda表达式语法
lambda表达式本质是一个匿名函数对象,与普通函数不同的是,它可以定义在函数内部。lambda表达式语法使用层而言,没有类型,我们一般使用auto或者模板参数定义的对象去接收lambda对象。
lambda表达式的格式:
[capture-list] (parameters)-> return type { function boby }
[capture-list]:捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据 [ ] 来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用,捕捉列表可以传值和传引用捕捉,捕捉列表不能为空!
(parameters):参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()一起省略。
->return type:返回值类型,用追踪返回类型形式,声明函数的返回值类型,没有返回值时此部分可以省略。一般返回值类型明确情况下,也可以省略,由编译器对返回类型进行推导。
{ function boby }:函数体,函数体内的实现与普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
int main()
{
// 一个简单的lambda表达式
auto add1 = [](int x, int y)->int {return x + y; };
cout << add1(1, 2) << endl;
// 1、捕捉为空也不能省略
// 2、参数为空可以省略
// 3、返回值可以省略,可以通过返回对象⾃动推导
// 4、函数体不能省略
auto func1 = []
{
cout << "111" << endl;
};
func1();
int a = 0, b = 1;
auto swap1 = [](int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
};
swap1(a, b);
cout << a << ":" << b << endl;
return 0;
}
2.捕捉列表
lambda表达式中默认只能用lambda函数体和参数中的变量,如果想用外层作用域中的变量,就要进行捕捉。
第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。[ x, y, &z]表示x和y值捕捉,z引用捕捉。
第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表中写一个=表示隐式值捕捉,在捕捉列表写一个&表示隐式引用捕捉,这样我们lambda表达式中用了哪些变量,编译器就会自动捕捉哪些变量。
第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[ =, &x ]表示其他变量隐式值捕捉,x引用捕捉。[ &, x, y ]表示其他变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第一个元素必须是&或=,并且&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引用捕捉。
int x = 0;
// 捕捉列表必须为空,因为全局变量不用捕捉就可以用,没有可被捕捉的变量
auto func1 = []()
{
x++;
};
int main()
{
// 只能用当前lambda局部域和捕捉的对象和全局对象
int a = 0, b = 1, c = 2, d = 3;
auto func1 = [a, &b]
{
// 值捕捉的变量不能修改,引用捕捉的变量可以修改
//a++;
b++;
int ret = a + b;
return ret;
};
cout << x << endl;
cout << func1() << endl;
// 隐式值捕捉
// 用了哪些变量就捕捉哪些变量
auto func2 = [=]
{
x++;
int ret = a + b + c + x;
return ret;
};
cout << func2() << endl;
// 隐式引用捕捉
// 用了哪些变量就捕捉哪些变量
auto func3 = [&]
{
a++;
x++;
d++;
};
func3();
cout << a << " " << b << " " << c << " " << d << " " << x << endl;
// 混合捕捉1
auto func4 = [&, a, b]
{
//a++;
//b++;
c++;
d++;
return a + b + c + d;
};
func4();
cout << a << " " << b << " " << c << " " << d << endl;
// 混合捕捉2
auto func5 = [=, &a, &b]
{
a++;
b++;
/*c++;
d++;*/
return a + b + c + d;
};
func5();
cout << a << " " << b << " " << c << " " << d << endl;
return 0;
}
运行结果:
0
2
5
1 2 2 4 2
1 2 3 5
2 3 3 5
lambda表达式如果在函数局部域中,他可以捕捉lambda位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉,lambda表达式中可以直接使用。这也意味着lambda表达式如果定义在全局位置,捕捉列表必须为空。
默认情况下,lambda捕捉列表是被const修饰的,也就是说传值捕捉过来的对象不能修改,mutable加在参数列表的后面可以取消其常量性,也就是说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改只影响形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。
int x = 0;
// 捕捉列表必须为空,因为全局变量不用捕捉就可以用,没有可被捕捉的变量
auto func1 = []()
{
x++;
};
int main()
{
int a = 0, b = 1, c = 2, d = 3;
// 局部的静态和全局变量不能捕捉,也不需要捕捉
static int m = 0;
func1();
auto func6 = []
{
int ret = x + m;
return ret;
};
cout << func6() << endl;
// 传值捕捉本质是⼀种拷⻉,并且被const修饰了
// mutable相当于去掉const属性,可以修改了
// 但是修改了不会影响外面被捕捉的值,因为是⼀种拷⻉
auto func7 = [=]()mutable
{
a++;
b++;
c++;
d++;
return a + b + c + d;
};
cout << func7() << endl;
cout << a << " " << b << " " << c << " " << d << endl;
return 0;
}
运行结果:
1
10
0 1 2 3
3.lambda的应用
学习lambda表达式之前,我们使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对会比较麻烦,使用lambda去定义可调用对象,既简单又方便。
lambda在很多其他地方用起来较好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等,lambda的应用还是很广泛的。
使用仿函数举例
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;
}
};
struct CompareEvaluateLess
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._evaluate < gr._evaluate;
}
};
struct CompareEvaluateGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._evaluate > gr._evaluate;
}
};
void PrintGoods(const vector<Goods>& v) {
for (const auto& good : v) {
cout << "[" << good._name << "] 价格: " << good._price << ", 评价: " << good._evaluate << endl;
}
cout << "-------------------------" << endl;
}
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3}, { "菠萝", 1.5, 4 } };
cout << "初始数据:" << endl;
PrintGoods(v);
// 1. 按价格升序
sort(v.begin(), v.end(), ComparePriceLess());
cout << "按价格 升序 排序后:" << endl;
PrintGoods(v);
// 2. 按价格降序
sort(v.begin(), v.end(), ComparePriceGreater());
cout << "按价格 降序 排序后:" << endl;
PrintGoods(v);
// 3. 按评价升序
sort(v.begin(), v.end(), CompareEvaluateLess());
cout << "按评价 升序 排序后:" << endl;
PrintGoods(v);
// 3. 按评价降序
sort(v.begin(), v.end(), CompareEvaluateGreater());
cout << "按评价 降序 排序后:" << endl;
PrintGoods(v);
return 0;
}
运行结果:
初始数据:
[苹果] 价格: 2.1, 评价: 5
[香蕉] 价格: 3, 评价: 4
[橙子] 价格: 2.2, 评价: 3
[菠萝] 价格: 1.5, 评价: 4
-------------------------
按价格 升序 排序后:
[菠萝] 价格: 1.5, 评价: 4
[苹果] 价格: 2.1, 评价: 5
[橙子] 价格: 2.2, 评价: 3
[香蕉] 价格: 3, 评价: 4
-------------------------
按价格 降序 排序后:
[香蕉] 价格: 3, 评价: 4
[橙子] 价格: 2.2, 评价: 3
[苹果] 价格: 2.1, 评价: 5
[菠萝] 价格: 1.5, 评价: 4
-------------------------
按评价 升序 排序后:
[橙子] 价格: 2.2, 评价: 3
[香蕉] 价格: 3, 评价: 4
[菠萝] 价格: 1.5, 评价: 4
[苹果] 价格: 2.1, 评价: 5
-------------------------
按评价 降序 排序后:
[苹果] 价格: 2.1, 评价: 5
[香蕉] 价格: 3, 评价: 4
[菠萝] 价格: 1.5, 评价: 4
[橙子] 价格: 2.2, 评价: 3
-------------------------
使用lambda表达式举例
// 类似这样的场景,我们实现仿函数对象或者函数指针支持商品中
// 不同项的比较,相对还是比较麻烦的,那么这里lambda就很好用了
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
// ...
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{
}
};
void PrintGoods(const vector<Goods>& v) {
for (const auto& good : v) {
cout << "[" << good._name << "] 价格: " << good._price << ", 评价: " << good._evaluate << endl;
}
cout << "-------------------------" << endl;
}
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "⾹蕉", 3, 4 }, { "橙⼦", 2.2, 3
}, { "菠萝", 1.5, 4 } };
// 1. 按价格升序
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._price < g2._price;
});
cout << "按价格 升序 排序后:" << endl;
PrintGoods(v);
// 2. 按价格降序
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._price > g2._price;
});
cout << "按价格 降序 排序后:" << endl;
PrintGoods(v);
// 3. 按评价升序
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate < g2._evaluate;
});
cout << "按评价 升序 排序后:" << endl;
PrintGoods(v);
// 4. 按评价降序
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate > g2._evaluate;
});
cout << "按评价 降序 排序后:" << endl;
PrintGoods(v);
return 0;
}
4.lambda的原理
lambda 的原理和范围for很像,编译后从汇编指令层的角度看,压根就没有 lambda 和范围for这样的东西。范围for底层是迭代器,而lambda底层是仿函数对象,也就说我们写了一个Lambda以后,编译器会生成一个对应的仿函数的类。
仿函数的类名是编译按一定规则生成的,保证不同的lambda生成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体,lambda的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是lambda类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传那些对象。
上面的原理,我们可以透过汇编层了解一下,下面第二段汇编层代码印证了上面的原理。
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);
cout << r1(10000,2) << endl;
cout << r2(10000, 2) << endl;
auto func1 = [] {
cout << "hello world" << endl;
};
func1();
return 0;
}
反汇编
// lambda
auto r2 = [rate](double money, int year) {
00B724C0 push ebp
00B724C1 mov ebp,esp
00B724C3 sub esp,0D4h
00B724C9 push ebx
00B724CA push esi
00B724CB push edi
00B724CC push ecx
00B724CD lea edi,[ebp-14h]
00B724D0 mov ecx,5
00B724D5 mov eax,0CCCCCCCCh
00B724DA rep stos dword ptr es:[edi]
00B724DC pop ecx
00B724DD mov dword ptr [this],ecx
00B724E0 mov ecx,offset _D4C5C408_Test@cpp (0B7F084h)
00B724E5 call @__CheckForDebuggerJustMyCode@4 (0B71398h)
00B724EA nop
return money * rate * year;
00B724EB mov eax,dword ptr [this]
00B724EE movsd xmm0,mmword ptr [money]
00B724F3 mulsd xmm0,mmword ptr [eax]
00B724F7 cvtsi2sd xmm1,dword ptr [year]
00B724FC mulsd xmm0,xmm1
00B72500 movsd mmword ptr [ebp-0D4h],xmm0
00B72508 fld qword ptr [ebp-0D4h]
};
00B7250E pop edi
00B7250F pop esi
00B72510 pop ebx
00B72511 add esp,0D4h
00B72517 cmp ebp,esp
00B72519 call __RTC_CheckEsp (0B713CFh)
00B7251E mov esp,ebp
00B72520 pop ebp
00B72521 ret 0Ch
讲解反汇编代码:
00B724C0 push ebp ; 1. 保存旧的栈底指针
00B724C1 mov ebp,esp ; 2. 设置新的栈底(栈帧建立)
00B724C3 sub esp,0D4h ; 3. 分配局部变量空间(包含调试用的缓冲区)
00B724C9 push ebx ; 4. 保存非易失性寄存器(惯例)
00B724CA push esi
00B724CB push edi
00B724CC push ecx ; 5. 【关键】压入 ecx。还记得吗?ecx 存的是 this 指针!
00B724CD lea edi,[ebp-14h] ; \
00B724D0 mov ecx,5 ; | (这部分是 VS 的 RTC 检查,用 0xCC 填充内存,
00B724D5 mov eax,0CCCCCCCCh ; | 用于检测越界,不用深究)
00B724DA rep stos dword ptr es:[edi]; /
00B724DC pop ecx ; 6. 恢复 ecx(重新拿到 this 指针)
00B724DD mov dword ptr [this],ecx ; 7. 把 this 指针存到一个名为 [this] 的临时变量里
00B724EA nop ; 调试占位符
; 步骤 1:取出 this 指针
00B724EB mov eax,dword ptr [this] ; eax = this 指针(指向 r2 对象的首地址)
; 步骤 2:加载第一个参数 money
00B724EE movsd xmm0,mmword ptr [money] ; xmm0 = money (10000.0)
; movsd 是移动双精度浮点数 (64bit)
; 步骤 3:【核心印证】访问成员变量 rate
00B724F3 mulsd xmm0,mmword ptr [eax] ; xmm0 = xmm0 * [eax]
; 【解释】
; eax 存的是 this 指针。
; [eax] 就是取 this 指向内存的前 8 个字节。
; 这正是我们按值捕捉的成员变量 _rate (0.49)!
; 此时 xmm0 = 10000.0 * 0.49
; 步骤 4:加载第二个参数 year (int -> double)
00B724F7 cvtsi2sd xmm1,dword ptr [year] ; cvtsi2sd = Convert Integer to Scalar Double
; xmm1 = (double)year (即 2.0)
; 步骤 5:最后一步乘法
00B724FC mulsd xmm0,xmm1 ; xmm0 = (10000*0.49) * 2
; 步骤 6:准备返回值
00B72500 movsd mmword ptr [ebp-0D4h],xmm0 ; 先把结果存回临时内存
00B72508 fld qword ptr [ebp-0D4h] ; 再把结果压入浮点栈 st(0)
; (这是 x86 下 double 返回值的标准传递方式)
00B7250E pop edi ; 恢复寄存器
00B7250F pop esi
00B72510 pop ebx
00B72511 add esp,0D4h ; 回收局部变量空间
00B72517 cmp ebp,esp ; 检查栈是否平衡(RTC 调试)
00B72519 call __RTC_CheckEsp (0B713CFh)
00B7251E mov esp,ebp ; 销毁栈帧
00B72520 pop ebp
00B72521 ret 0Ch ; 【关键】返回并清栈
; ret 0Ch = ret + add esp, 12
; 为什么是 12 字节?
; 参数 money (double, 8字节) + year (int, 4字节) = 12字节。
; 这完美符合函数签名 `double lambda(double, int)`。
本章完。
更多推荐
所有评论(0)