时间:2023-01-26 09:46:23 | 栏目:C代码 | 点击:次
C++要走向现代语言,如果不支持lambda表达式,你很难认为这门语言和现代有什么关系。幸好,从C++11标准起,它就实现了对lambda表达式的支持。
那么,什么是lambda表达式呢?
lambda表达式是匿名函数,就是说不用定义函数名,函数实现可以直接嵌入在业务逻辑代码中。诸如python、java、C#等语言,都将其作为基础特性。
其优点是提高了代码的可读性,对于一些无需重用的方法特别适合。例如在容器的迭代中实现特定的查询逻辑。
C++11标准中,对于lambada表达式的定义如下:
[captures] (params) specifiers exception -> ret {body}
从上面的定义可以看到,lambda表达式的语法多少有些与我们以往的认知不太一样。所以,我们直接上代码来体会吧。
#include <iostream> int main() { int x = 10; auto foo = [x](int y)->int { return x + y; }; std::cout << foo(7) << std::endl; }
各位不要手懒,务必打开IDE将这段代码运行一下,看看结果。然后再尝试修改一下参数类型,或者返回类型。
增加mutable之后,对x变量进行修改,看看会发生什么?
int main() { int x = 10; auto foo = [x](int y) mutable ->int { x++; return x + y; }; std::cout << foo(7) << std::endl; std::cout << foo(7) << std::endl; std::cout << foo(7) << std::endl; }
而一个最简形式的lambda表达式,可以是 auto foo = [] { return 0; };
。
所以大家以后看到类似语法,可不要大惊小怪了,还以为这是什么另类的数组访问方式,或者琢磨->这个指针指向了个什么东西。
毫无疑问,lambda表达式中,最反直觉的就是捕获列表的定义。毕竟在我们的认知里,中括号是用来定义数组并访问数组元素的。
而且捕获列表的诸多特性,也是面试中挖坑的好地方。我们先从作用域开始说起。
关于lambda表达式的作用域,有四个重点:
好,为了加强印象,我们先从面试挖坑场景开始。请先看第一坑:
int main() { int x = 10; auto foo = [x](int y) ->int { x++; return x + y; }; std::cout << foo(8) << std::endl; }
问:上述代码能通过编译吗?如果不能,是为什么?
答:不能。因为根据规则一和二,在表达式函数体内,x变量是只读,不能被修改。
好,上面的只是开胃菜,诡异的第二坑来了:
int main() { int x = 10; auto foo = [x](int y) mutable ->int { x++; std::cout << x << std::endl; return x + y; }; foo(8); foo(8); std::cout << x << std::endl; }
问:在上述代码执行完后,请说出所有std::cout
语句输出什么内容?
答:根据规则三,分别输出11,12,10。
所以大家可以看到,前两次输出是在表达式体内对x值进行修改,x的状态是保留的了。但在函数体外,x变量的值仍然保持不变。千万要记住这个规则,这就是存在两个作用域所得到的结果。
后面我们会讲解其实现原理,这样就可以方便记忆这条规则了。
再来第三坑:
int y = 100; int main() { static int x = 10; auto foo = [x, y](int z) ->int { return x + y + z; }; std::cout << foo(8) << std::endl; }
问:上述代码有什么问题,应该如何调整?
答:根据作用域规则四,x与y都不能作为lambda表达式的捕获列表变量。在表达式函数体内可以直接使用静态或者全局变量,所以只要修改为auto foo = [](int z) ->int { return x + y + z; };
即可。
虽然上面这段代码放在gcc编译器下不会报错,只是出警告:capture of variable ‘x’ with non-automatic storage duration
。但编译器实际上是作了一次选择,即认为你的意图是作为全局或静态变量来使用。可如果你只是手误或者忘了要调整代码,那就会出现预料之外的运行结果。
实际上,笔者所在公司就严格要求不能出现编译警告,这对于防止运行期出现偏差是非常重要的。
上一节我们所讲的,都是捕获值的用法。那么想将变量作为引用传递进lambda表达式,是否可以呢?
答案是肯定的,示例如下:
int main() { int x = 10; int y = 11; auto foo = [&x, &y](int z) mutable ->int { x++; y++; return x + y + z; }; std::cout << foo(8) << std::endl; std::cout << x << std::endl; std::cout << y << std::endl; }
可以看到,捕获引用就是在变量前加上&符号即可。切记不要与取地址相混淆。被引用变量在函数体内修改,也会影响函数体外同名变量的值,这一点与我们以往的认知相同。
至于上述代码结果,请各位先在脑海里执行一遍,然后上机验证。
捕获列表支持三种特殊情况,下面分别说明:
先看[this]的示例:
class CaptureOne { public: void test() { auto foo = [this] { std::cout << x << std::endl; }; foo(); } private: int x = 100; }; int main() { CaptureOne one; one.test(); }
建议大家在上面这个例子里继续扩展,例如增加一个类方法,看看在表达式函数中如何调用,以及执行结果是什么。
再看[=]的示例:
int main() { int x = 10; int y = 11; int z = 12; auto foo = [=]() mutable ->int { x++; y++; return x + y + z; }; std::cout << foo() << std::endl; std::cout << x << std::endl; std::cout << y << std::endl; }
可以看到,这的确是方便了代码书写。如果表达式函数体内的变量增加或减少,都不必再费心思去调整捕获列表了。
另外,不知道大家还注意到一个细节没有,就是当参数列表为空时,如果存在mutable标识,则()是不能省略的。
最后,[&]还是烦请各位自己去实践并体验。
C++多年老手,在上手lambda表达式的时候,应该会马上在脑海里跟函数对象关联起来。
我们先从定义一个函数类开始说起。所谓函数类,就是定义一个类,然后重载operator ()。这样可以将类对象作为函数一样来调用。
示例如下:
class FuncOne { public: FuncOne(int x, int y) : valx(x), valy(y){} int operator () () { return valx + valy; } private: int valx; int valy; }; int main() { FuncOne one(10, 20); std::cout << one() << std::endl; }
上述代码就是函数类的定义与函数对象的使用方法。而C++11标准中的lambda表达式,其实就是在编译器在编译期生成一个闭包类(可以理解为类似于FuncOne的类),然后在运行时由这个闭包类生成一个对象,称之为闭包。
而闭包就是一个匿名的函数对象,它包含了定义时作用域的上下文。这样我们就会发现lambda表达式,其实就是C++11给我们提供的一个语法糖。
现在再回头去理解捕获列表的四条原则,是否有恍然大悟之感?理解了原理,对于原则就不用死记硬背了,祝各位面试好运!
当然,我们不能满足于只过面试关,还应当在工作中用好它才行。做C++开发的,用好STL是基本功,而lambda表达式与STL相结合,可以简化代码,发挥出强大的威力来。
先看一个需求:我们要在一组vector列表中,打印可以整除4的数。
示例代码:
#include <iostream> #include <vector> #include <algorithm> int main() { std::vector<int> num_list = {1, 3, 9, 10, 12, 17, 18}; std::cout << *std::find_if(num_list.cbegin(), num_list.cend(), [](int i) { return (i % 4) == 0;}) << std::endl; }
大家可以看到,如果是在以前,要实现这个功能,需要额外写多少代码。
也希望各位能从这个简单示例开始,回头去梳理自己以前写的代码,可以怎样优化结构并提高效率。并且在将来的开发过程中,不断修炼功夫,成为C++大咖!