盐酸的C++基础知识小讲堂——lambda表达式入门
真实基础知识,真实讲堂。
以下笔记摘自 D.N.Code 群聊天记录。
如有错误请指出。
看维基百科的 Perm 算法的实现时发现了一些不能理解的东西。
1 | explicit perm(const int l = 0, function<void(vector<int>&)> fun = [](vector<int>&) {}) : |
这一堆东西是什么???
建议去看一下lambda表达式(
——盐酸
之后 jy 提问“[&](vector<int>& vec)
是什么,为什么要用,要怎么用”。由此引出盐酸的 C++ 基础小知识讲座之 lambda 表达式。
lambda 表达式
什么是 lambda 表达式
lambda 表达式是 C++11 起加入的新功能。它构造闭包,能够捕获作用域中的变量的无名函数对象。换句话说,可以将 lambda 表达式理解成一个没有名字的内联函数。
一个完整的 lambda 表达式长这样:
1 | [ 捕获 ] <模板形参>(可选)(C++20) ( 形参 ) 说明符(可选) 异常说明 attr -> return requires(可选)(C++20) { 函数体 } |
还有以下写法:
1 | [ 捕获 ] ( 形参 ) -> return { 函数体 } |
不管忽略什么,[]
和函数体都永远不可少。其中, []
是用来捕获不属于 lambda 表达式的变量的,即外部变量。如果留空,就代表 lambda 表达式不捕获外部变量。可以在 []
里加变量名、 &
或者 =
,这样就能捕获外部变量了。
假设有一个外部变量名为 a
。[a]
意为使用值捕获的方式捕获它,[&a]
意为使用引用捕获的方式捕获它。跟函数的值传递和引用传递一样。
加 &
或者 =
的两种做法叫做隐式捕获,由编译器自己去推断要捕获的变量。[&]
的意思是,如果编译器发现 lambda 函数体里面有编译器不认识的东西,那编译器就到外面那层去找有没有叫这个名字的,如果有就引用捕获它。[=]
则是把引用捕获改成值捕获。
如,
1 | std::vector<int> c = {1, 2, 3, 4, 5, 6, 7}; |
这就定义了一个匿名函数,参数是 int i
,作用是输出 i
+空格。这个 lambda 表达式的 []
是空的,说明它不捕获外部变量。
而
1 | void print_plus_x(const std::vector<int>& v, const int x) |
则显式捕获了外部变量 x
。
1 | void print_plus_x(const std::vector<int>& v, const int x) |
把 [x]
改成 [&]
之后运行,结果与写成 [x]
一致。这就是引用隐式捕获。
另外需要注意的是,当混合使用显式捕获和隐式捕获的时候,捕获列表的第一个元素必须是一个 &
或者 =
。
捕获 (capture) 是什么?x 和 i 的区别在哪
capture 的意思就是把这个函数体外面的东西捕获进来使用。
——盐酸
x 和 i 的区别在于,i 是 lambda 表达式自己的参数;而 x 不属于 lambda 表达式,它是外部变量,是 lambda 表达式所在函数定义的局部变量。
为什么要用 lambda 表达式
所以,你懒得对这个“事情”定义一个专门的函数,然后就 inline 定义一个函数。
——jy
比如说你懒得定义一个函数名的时候。因为这玩意就用一遍,也很简单,所以一个 lambda 比较简洁。
——夜轮
比如你想搞一个函数,输入一个 vector 和一个 x,输出 vector 里每个元素 + x,那你这里 for_each 的第三个参数就是那玩意。因为 x 不是参数,i 才是那个参数,for_each 想要的函数是接受一个参数的,他只往函数里面传那个 i 进去,但是你又想在函数体里面用到 x,那就把 x 抓进函数体里面。
——盐酸
对于那种只在一两个地方使用的简单操作,lambda 表达式是最有用的。如果要在很多地方使用相同的操作,或者一个操作需要很多语句才能完成,通常建议使用函数。
——《C++ Primer Plus》
像这种函数能定义返回值吗
可以的。甚至可以加个模板(。只要在函数体里写上 return
就有了。如果需要定义 lambda 表达式的返回值的话,还得把 -> return
这个地方写好。这叫做尾置返回类型,是 C++11 里和 lambda 表达式里一起引入的新写法。普通函数也可以用,但 lambda 表达式要定义返回类型的话,必须用尾置返回类型。还是举例说明吧。
1 | std::for_each(c.begin(), c.end(), [](int i){ std::cout << i << ' '; }); |
这里不用写返回值,是因为编译器看 lambda 表达式没 return,推断出来返回值类型是 void
。如果改成这样:
1 | std::for_each(c.begin(), c.end(), [](int i){ |
return i
,编译器就能看出来返回值类型是 int
,因为 i
是个 int
,lambda 表达式就会返回整数 i
。另外,写了 ->return type
但是不写 return
语句的话,编译器会不给过编译。
对 Perm 函数的完整解释
请移步至Perm 算法。
延伸
盐酸讲 high 了的延伸部分
C++ 的 lambda 表达式跟其他很多语言不同之处在于 C++ 的 lambda 是 0 overhead 的(((就是无额外开销
大部分语言的lambda都是类似这个
&
(并且自带 type erasure,这里就有 runtime overhead 了)。为什么会有 overhead 呢,type erasure 必定带来 overhead,因为 type erasure 的目的是把所有返回值是Res
而参数是Arg...
的东西都擦除成同一个类型。这就必定需要到堆上申请空间,因为捕获的变量不同所需要的空间也就不同,然而同一个类型在栈上的空间是相等的。py 那个 lambda 严重缩水,那个叫 list comprehension(
py那个函数定义本身就是个 closure
1
2
3
4 def plus(x, y):
def plus_x(v):
return v + x
return plus_x(y)比较心把但是应该懂我意思,就是 py 的函数自动捕获外面的变量(
py 所有函数都捕获外部变量,C++ 那个例子如果你中括号里没有 x 的话是会编译错误的((((
他就是原地定义了一个匿名的类,那个 lambda 是这个类的一个对象
注:
- type erasure:类型擦除,指在编译期明确去掉所编程序(某部分)的类型系统
- runtime overhead:运行时开销
- list comprehension:递推式构造列表
锦鳞的一个问题
我记得 lambda 表达式不完全等于匿名函数,那区别在哪((
盐酸学长已经在这篇 blog 里说了。请移步阅读。
对《C++ Primer Plus》的一个疑问
《C++ Primer Plus》里举了个强制指定 lambda 表达式返回值的例子:
1
2
3
4
5 transform(vec.begin(), vec.end(), vec.begin(), [](int i)
{
if (i < 0) return -1;
return 1;
});编译器推断这个版本的 lambda 表达式返回类型为
void
,但它返回了一个int
类型的值。应该在(int i)
后面加上-> int
。
在 Visual Studio 里试了一下不加的情况。程序完全能正常跑,IDE 识别出来了返回类型为 int
。迷惑。