简介 & 用法
lambda表达式是c++11引入的一个重要特性,基本语法如下
[捕获列表](形参列表) -> 返回类型 {// 函数体
}
其中捕获列表和形参列表可以为空,返回值类型大部分情况下可以忽略不写。
lambda表达式的结构整体上和普通函数一样,特殊在比普通函数多一个捕获列表。捕获列表的作用是获取作用域以外的局部变量给函数体使用。捕获器有两种形式:
- 按值捕获:语法
[localValue]
,原理和形参列表的按值传递类似。 - 按引用捕获:语法
[&localValue]
,原理和形参列表的按引用传递类似。
举个例子
int main {int base = 100;int count = 0;auto trans = [base, &count](int value) {++count;return base + value;};int res = trans(5);printf("base=%d, count=%d, res=%d\n", base, count, res);
}
示例代码中的lambda表达式trans
按值捕获局部变量base
、按引用捕获局部变量count
,因此lambda表达式内部修改base
不会影响外部base
的值,而修改count
则会影响外部count
的值。
lambda表达式的原理
本质:lambda本质上是函数对象(函数对象的介绍见本文最后一节 扩展知识)。
为了弄清楚lambda表达式的原理,我们需要研究研究示例代码的汇编码
main::{lambda(int)#1}::operator()(int) const:push rbp...ret
main:push rbp...call main::{lambda(int)#1}::operator()(int) const...ret
函数调用语句int res = trans(5);
对应的汇编码为call
指令,调用的函数符号main::{lambda(int)#1}::operator()(int)
代表的是匿名类main::{lambda(int)#1}
的函数调用操作符operator()
。
函数操作符operator()
?这不是函数对象嘛!为了一探究竟,我们用函数对象重写示例代码对比两者的汇编码看看
int main() {int base = 100;int count = 0;// auto trans = [base, &count](int value) {// ++count;// return base + value;// };struct TransLambda {TransLambda(int& count) : _count(count) { }int operator()(int value) {++_count;return _base + value;}int& _count;int _base;};TransLambda trans(count);int res = trans(5);
}
这段代码的汇编码如下
main::TransLambda::TransLambda(int&) [base object constructor]:push rbp...ret
main::TransLambda::operator()(int):push rbp...ret
main:push rbp...call main::TransLambda::TransLambda(int&) [complete object constructor]...call main::TransLambda::operator()(int)...leaveret
对比lambda表达式版本和函数对象两个版本的汇编码,发现两段汇编码的内容几乎一样。破案了,lambda表达式就是函数对象,也可以认为lambda是函数对象的语法糖。
为了进一步理解,我们不妨大胆猜测一下编译器拿到lambda函数后做了几件事
- 定义匿名类
main::{lambda(int)#1}
。 - 参考lambda表达式的内容,重载
main::{lambda(int)#1}
的函数调用操作符operator()
。 - 为匿名类
main::{lambda(int)#1}
分配一个实例,并赋值给变量trans
。 - 调用实例
trans
的函数调用操作符operator()
。
所以lambda表达式实际上是语法更为简洁的函数对象,它的特殊部分捕获列表实际上是传给函数对象的成员变量
- 按值捕获:相当于按值传参,函数对象持有的是这个变量的拷贝。
- 按引用捕获:相当于按引用传参,函数对象持有的是这个变量的引用。
- 捕获列表和形参列表的区别在于 捕获列表捕获到的变量由函数对象的成员变量维护,生命周期于函数对象一样;而形参列表的入参生命周期是函数级别的。
避坑指南
static修饰的lambda表达式不能按引用捕获局部变量。
例如
std::string GetString() {std::string id = "Test";static auto appendId = [&id](std::string value) {return name + "-" + value;};std::string res = appendId("Hello");
}int main() {std::string res1 = GetString();std::string res2 = GetString(); // 可能引发奔溃
}
原因很简单,lambda表达式appendId
按引用捕获局部变量id
,所以appendId
会持有id
的引用,但是因为appendId
是static
修饰的所以生命周期比局部变量id
长,当id
销毁以后appendId
持有的id
引用实际上已经销毁了,出现未定义的行为。
(其他坑待补充…)
扩展知识
函数对象(仿函数)
函数对象,也叫仿函数。如果一个类重载了函数调用操作符operator()
,这个类的实例就叫函数对象,可以像使用函数一样使用这个对象。例如
Struct Transform {int operator()(int value) {++count;return 100 + value;}int count = 0;
};int main() {Transform trans;int res = trans(5);
}
Transform
的实例trans
就是一个函数对象,我们可以像调用函数一样使用trans
。函数对象的特殊之处在于可以保持状态,例如trans
可以通过成员变量count
统计自己被调用过多少次,而普通函数做不到这一点。