当前位置:   article > 正文

c++ 11 lambda 用法与原理_lamda值传递

lamda值传递

lambda也出现了好长时间,一直以来也仅仅限于使用,今天,借助此文,我们从使用、实现的角度聊聊lambda。

在开始正文之前,我们先看一个问题,对下面的vector进行排序:

std::vector<int> v = {132};

在C++11之前,我们可能会这么做(普通函数,即函数指针作为参数):

  1. bool Compare(int a, int b) {
  2.   return a < b;
  3. }
  4. int main() {
  5.   std::vector<int> v = {132};
  6.   std::sort(v.begin(), v.end(), Compare);
  7.   
  8.   return 0;
  9. }

也有可能这样做(函数对象,即类对象作为参数):

  1. int main() {
  2.   struct Compare {
  3.     bool operator()(int a, int b) {
  4.       return a < b;
  5.     }
  6.   };
  7.   std::vector<int> v = {132};
  8.   std::sort(v.begin(), v.end(), Compare());
  9.   return 0;
  10. }

但是上述两种方式均有其局限性,对于普通函数的实现方式来说,优点是具有最小的语法开销,缺点是不能限定作用域(即必须在使用作用域外进行定义),而对于函数对象的实现方式来说,优点是可以在作用域内进行定义,但缺点是需要有类定义的语法开销

既然函数指针和函数对象都有其优缺点,那么有没有其它方式既保持了二者的优点,又摒弃了二者的缺点呢?当然有了,这就是lambda

本文的主要内容如下:

概念

自C++11开始,引入了lambda(一般称之为为lambda表达式),一个lambda表达式表示一个可调用的代码单元。我们可以将其理解为一个匿名的内联函数。lambda表达式跟普通函数相比不需要定义函数名,取而代之的多了一对方括号[]

先看下lambda的基本语法,如下:

[capture](parameters) specifiers exception attr -> return type { /*code*/ }

在上面定义中:

  • [capture]代表捕获列表,括号内为外部变量的传递方式,包括值传递、引用传递等

  • (parameters)代表参数列表,其中括号内为形参,和普通函数的形参一样

  • specifiers exception attr代表附加说明符,一般为mutablenoexcept

  • ->return type代表lambda函数的返回类型如 -> int-> string等。在大多数情况下不需要,因为编译器可以推导类型

  • {}内为函数主体,和普通函数一样

为了便于我们对lambda的使用有个初步认识,下面是一些常用的例子:

  1. // 1. 最简单的lambda,没有任何行为操作:
  2. []{};
  3. // 2. 包含两个参数的lambda:
  4. [](float f, int a) { return a * f; };
  5. [](int a, int b) { return a < b; };
  6. // 3. 有返回值的lambda:
  7. [](MyClass t) -> int { auto a = t.compute(); print(a); return a; };
  8. // 4. 存在附加说明符的lambda:
  9. [x](int a, int b) mutable { ++x; return a < b; };
  10. [](float param) noexcept { return param*param; };
  11. [x](int a, int b) mutable noexcept { ++x; return a < b; };
  12. // 5. 参数列表可选:
  13. [x] { std::cout << x; }; // 去掉()
  14. [x] mutable { ++x; };    // 编译失败!
  15. [x]() mutable { ++x; };  // 正常编译,这是因为在附加说明符前面需要有()
  16. [] noexcept { };        // 编译失败!
  17. []() noexcept { };      // 正常编译,这是因为在附加说明符前面需要有()

好了,现在回到正题,如果我们使用lambda来实现之前排序的话,应该怎么做呢?如下:

  1. int main() {
  2.   std::vector<int> v = {132};
  3.   std::sort(v.begin(), v.end(), [](int a, int b){
  4.     return a < b;
  5.   });
  6.   return 0;
  7. }

从上述实现可以看出,其相较于函数指针函数对象的实现方式,更为简洁直观

捕获列表

在上一节中,我们提到了lambda定义中的几个基本点:捕获列表函数参数附加说明符返回类型以及函数体。函数参数、返回类型和函数体在普通函数或者类成员函数中我们都有用到,那么什么是捕获列表和附加说明符呢?这就是本节的内容。

捕获的作用是捕获lambda所在函数的局部变量(捕获全局变量或者静态变量编译器会报warning,后面有说明)。其中捕获的类型可以分为值捕获,引用捕获和隐式捕获:

  • 值捕获 与函数中的值传递类似。lambda表达式捕获的是变量的一个拷贝,因此我们如果在lambda表达式后面改变该变量值的话,不会影响捕获前的该变量值,这就是所谓的值捕获

    1. int a = 1;
    2. [a](){printf("%d\n", a;);}
  • 引用捕获 引用捕获和值捕获形式完全一样,只是在捕获列表中传的是变量的引用,类似于函数中的引用传递,变成下面这个样子

    1. int a = 1;
    2. [&a](){printf("%d\n", a;);}
  • 隐式捕获的方式,就是捕获的列表可以用=&代替,让编译器隐式的推断你使用的是哪个变量,然后这两个字符表示捕获的类型=表示值捕获,&是引用捕获;写出来之后就变成了如下的形式:

    1. int a = 1;
    2. [=](){printf("%d\n", a);};
    3. [&](){printf("%d\n", a;);}

下面是捕获列表的一些语法规则:

  • [&]通过引用捕获作用域内的全部局部变量

  • [=]通过引用捕获作用域内的全部局部变量

  • [x, &y] x按照值捕获和y按照引用捕获。

  • [x = expr] 带有初始化表达式的捕获 (C++14)

  • [args...] 捕获模板参数包,全部按值。

  • [&args...] 捕获模板参数包,全部通过引用。

  • [...capturedArgs = std::move(args)](){} 通过移动操作符捕获包(C++20)

捕获规则示例代码如下:

  1. int x = 2, y = 3;
  2. const auto l1 = []() { return 1; };   // 没有捕获任何内容 
  3. const auto l2 = [=]() { return x; };  // 按值捕获所有变量
  4. const auto l3 = [&]() { return y; };  // 按引用捕获所有变量
  5. const auto l4 = [x]() { return x; };  // 仅对x进行按值捕获
  6. const auto l5 = [&y]() { return y; }; // 仅对y进行按引用捕获
  7. const auto l6 = [x, &y]() { return x * y; }; // 对x按值捕获,对y按引用捕获
  8. const auto l7 = [=&x]() { return x + y; }; // 对x按引用捕获,其余的按值捕获
  9. const auto l8 = [&, y]() { return x - y; };  // 对y按值捕获,其余的按引用捕获
  10. const auto l9 = [this]() { } // 捕获this指针
  11. const auto la = [*this]() { } // 按值捕获*this对象

值捕获

lambda表达式可以将作用域内的变量捕获到lambda函数中。在lambda的表达式定义中,我们有提到[=]指定可以按值捕获作用域内的任何变量[x]则仅仅按值捕获变量x

仅捕获某个变量,代码如下:

  1. int main() {
  2.   int x = 5;
  3.   auto fun = [x]() { printf("%d\n", x); };
  4.   fun();
  5.   return 0;
  6. }

捕获所有变量,代码如下:

  1. int main() {
  2.   int x = 5;
  3.   int y = 6;
  4.   auto fun = [=]() { printf("%d, %d\n", x, y); };
  5.   fun();
  6.   return 0;
  7. }

引用捕获

可以使用引用捕获调用lambda表达式。当使用引用捕获时候,捕获的值实际上是对lambda外部范围内变量的引用。

  1. int main() {
  2.   int x = 5;
  3.   auto fun = [&x]() { printf("%d\n", ++x); };
  4.   fun();
  5.   printf("%d\n", x);
  6.   return 0;
  7. }

输出如下:

  1. 6
  2. 6

如果外部变量很多,想按引用捕获外部所有变量的话,可以使用[&]方式,如下:

  1. int main() {
  2.   int x = 5;
  3.   int y = 0;
  4.   auto fun = [&]() { printf("%d, %d\n", ++x, --y); };
  5.   fun();
  6.   printf("%d, %d\n", x, y);
  7.   return 0;
  8. }

输出如下:

  1. 6 -1
  2. 6 -1

mutable关键字

本来mutable关键字应该单列一节来进行说明,但是因为其与捕获列表关系紧密,所以就暂时放在了本节一起来进行说明。

我们经常有一种需求,需要对某个变量进行修改,或者说局部范围内的修改,当退出该作用域的时候,变量又恢复原值。对于这种需求,我们可以尝试使用值捕获来完成,代码如下:

  1. int main() {
  2.   int x = 5;
  3.   auto fun = [x]() { printf("%d\n", ++x); };
  4.   fun();
  5.   printf("%d\n", x);
  6.   return 0;
  7. }

编译之后,发现编译器会报错,如下:

  1. 错误:令只读变量‘x’自增
  2. auto fun = [x]() { printf("%d\n", ++x); };

从上述编译器的输出来看,对于按值捕获的变量,编译器会将其设置为只读(read only),所以对只读变量进行尝试修改的操作是不被编译器所允许的,而mutable 则可以解决此类错误,如下:

  1. int main() {
  2.   int x = 5;
  3.   auto fun = [x]() mutable { printf("%d\n", ++x); };
  4.   fun();
  5.   printf("%d\n", x);
  6.   return 0;
  7. }

代码输出如下:

  1. 6
  2. 5

捕获全局变量和静态变量

一般情况下,lambda是用来捕获局部变量的,如果用其来捕获全局变量或者静态变量,那么编译器会报warning ,如下代码:

  1. #include <iostream>
  2. #include <vector>
  3. #include <algorithm>
  4. int x = 4;
  5. int main() {
  6.   auto fun = [x]() { printf("%d\n", x); };
  7.   fun();
  8.   return 0;
  9. }

编译器输出如下:

  1. test.cc: In function ‘int main()’:
  2. test.cc:7:15: warning: capture of variable ‘x’ with non-automatic storage duration
  3.     7 |   auto fun = [x]() { printf("%d\n", x); };
  4.       |               ^
  5. test.cc:5:5: note: ‘int x’ declared here
  6.     5 | int x = 4;
  7.       |     ^

捕获初始化表达式

自C++14开始,在捕获列表中可以使用初始化表达式,也就是说可以创建新的变量并在捕获子句中对其进行初始化。这种方式称之为带有初始化程序的捕获或者广义lambda捕获

  1. int main() {
  2.   int x = 1;
  3.   int y = 2;
  4.   auto fun = [z = x + y]() { printf("%d\n", z); };
  5.   fun();
  6.   return 0;
  7. }

在上面的例子中,编译器生成一个新的成员变量并用x+y对其进行初始化,也就是是说上面示例等价于:

  1. int main() {
  2.   int x = 1;
  3.   int y = 2;
  4.   int z = x + y;
  5.   auto fun = [z]() { printf("%d\n", z); };
  6.   fun();
  7.   return 0;
  8. }

混合捕获

混合捕获,还是比较好理解的,话不多说,直接上代码:

  1. int main() {
  2.   int x = 1;
  3.   int y = 2;
  4.   auto fun = [x, &y](){
  5.     printf("%d, %d\n", x, ++y);
  6.   };
  7.   fun();
  8.   
  9.   return 0;
  10. }

在上述代码中,对x进行按值捕获,而堆y则进行按引用捕获。

编译器实现

经常看我文章的读者,可能发现我的文章有个特点,喜欢说明白底层实现,其实这也是C++开发人员的一个特点,知其然,更要知其所有然,毕竟知己知彼,方能百战不殆嘛。

好了,言归正传,开始聊聊lambda的底层实现。那么我们该如何知道编译器的底层是如何实现的呢?在这里推荐一个工具cppinsights,是一款C++源代码到源代码的转换,它可以把C++中的模板、auto以及C++11新特性展开。通过使用cppinsights,我们可以清楚地看到编译器做了哪些事情。

值捕获

仍然使用前面的代码,如下:

  1. int main() {
  2.   int x = 5;
  3.   auto fun = [x]() { printf("%d\n", x); };
  4.   fun();
  5.   return 0;
  6. }

cppinsights输出如下:

  1. int main()
  2. {
  3.   int x = 5;
  4.     
  5.   class __lambda_8_14
  6.   {
  7.     public
  8.     inline /*constexpr */ void operator()() const
  9.     {
  10.       printf("%d\n", x);
  11.     }
  12.     
  13.     private
  14.     int x;
  15.     
  16.     public:
  17.     __lambda_8_14(int & _x)
  18.     : x{_x}
  19.     {}
  20.     
  21.   };
  22.   
  23.   __lambda_8_14 fun = __lambda_8_14{x};
  24.   fun.operator()();
  25.   return 0;
  26. }

从上面内容,我们可以看出,编译器针对lambda会生成一个类__lambda_8_14,然后调用该类的成员函数:

  • __lambda_8_14为由编译器针对lambda函数生成的一个类

  • __lambda_8_14定义了一个成员变量x,其初始值为

  • __lambda_8_14重载operator()其函数体为lambda函数体(本例中为printf("%d\n", x))

  • 源码中的fun在编译器实现之后,变成了一个__lambda_8_14对象

  • 对fun函数的调用,变成了调用__lambda_8_14对象的operator()函数

如果捕获列表内容为[=],则类的private成员变量中会包含范围内的且在lambda中被使用的局部变量。假如有x和y两个变量,如果只使用了x这个变量,那么private成员变量就只有x,反之如果都使用了,则成员变量就变成了x和y。

如下代码:

  1. int main() {
  2.   int x = 5;
  3.   int y = 6;
  4.   auto fun = [=]() { printf("%d, %d\n", x, y); };
  5.   fun();
  6.   return 0;
  7. }

上述代码的lambda部分,经过编译器编译之后,会变成如下:

  1. class __lambda_9_14
  2.   {
  3.     public
  4.     inline /*constexpr */ void operator()() const
  5.     {
  6.       printf("%d, %d\n", x, y);
  7.     }
  8.     
  9.     private
  10.     int x;
  11.     int y;
  12.     
  13.     public:
  14.     __lambda_9_14(int & _x, int & _y)
  15.     : x{_x}
  16.     , y{_y}
  17.     {}
  18.     
  19.   };

在捕获列表中使用[=],但是lambda实现体内只使用变量x,那么编译器又将如何操作呢?

  1. int main() {
  2.   int x = 5;
  3.   int y = 6;
  4.   auto fun = [=]() { printf("%d\n", x); };
  5.   fun();
  6.   return 0;
  7. }

编译器对lambda部分的实现如下所示:

  1. class __lambda_9_14
  2.   {
  3.     public
  4.     inline /*constexpr */ void operator()() const
  5.     {
  6.       printf("%d\n", x);
  7.     }
  8.     
  9.     private
  10.     int x;
  11.     
  12.     public:
  13.     __lambda_9_14(int & _x)
  14.     : x{_x}
  15.     {}
  16.     
  17.   };

上述输出中可见,对于[=]捕获方式,如果函数体内没有使用的变量,编译器不会生成对应的成员变量

引用捕获

在上述值列表中,编译器会生成对应的成员变量,这样成员变量是对值列表中对应变量的一个拷贝,那么如果是引用列表,则成员变量则是对应变量的一个引用

  1. int main() {
  2.   int x = 5;
  3.   auto fun = [&x]() { printf("%d\n", ++x); };
  4.   fun();
  5.   printf("%d\n", x);
  6.   return 0;
  7. }

lambda部分经过编译器操作之后,如下:

  1. class __lambda_8_14
  2.   {
  3.     public: 
  4.     inline /*constexpr */ void operator()() const
  5.     {
  6.       printf("%d\n"++x);
  7.     }
  8.     
  9.     private: 
  10.     int & x;
  11.     
  12.     public:
  13.     __lambda_8_14(int & _x)
  14.     : x{_x}
  15.     {}
  16.     
  17.   };

可以看到,成员变量部分是引用列表中的引用,即int &x

如果列表为[&],则编译器将会生成对应变量的引用,规则与值列表类似,在此不再赘述。

C++中,const对成员变量的影响是:- const函数不能修改普通成员变量的值
- 但可以修改可变(mutable)成员变量
- 也可以修改引用类型的成员变量(因为引用本身是可以修改的)所以引用类型的n作为成员变量,不受const约束,可以在const operator()中被修改。这就是编译器특意设置的一个机制,来实现通过引用捕获访问外部变量的效果。

总结一下:

1. 引用捕获使外部变量成为lambda类的引用型成员
2. 引用类型成员不受const约束
3. 所以可以在const operator()中修改,所以引用捕获不用加mutable

mutable关键字

在前面内容中,可以看到,无论是按值捕获还是按引用捕获,编译器都会生成一个成员函数operator(),且被声明为const ,这也就意味着不能修改成员变量。

如果要修改此行为,则需要在参数列表后添加mutable关键字,这样就可以将const从operator()函数的声明中去除。

  1. int main() {
  2.   int x = 5;
  3.   auto fun = [x]() mutable { printf("%d\n", ++x); };
  4.   fun();
  5.   return 0;
  6. }

上述lambda在编译器中的实现如下:

  1.  class __lambda_8_14
  2.   {
  3.     public: 
  4.     inline /*constexpr */ void operator()()
  5.     {
  6.       printf("%d\n"++x);
  7.     }
  8.     
  9.     private: 
  10.     int x;
  11.     
  12.     public:
  13.     __lambda_8_14(int & _x)
  14.     : x{_x}
  15.     {}
  16.     
  17.   };

混合捕获

混合列表是值列表和引用列表的一种组合,了解了这两种实现,混合列表的编译器实现就更好理解了。

  1. int main() {
  2.   int x = 1;
  3.   int y = 2;
  4.   auto fun = [x, &y](){
  5.     printf("%d, %d\n", x, ++y);
  6.   };
  7.   fun();
  8.   
  9.   return 0;
  10. }

lambda部分编译器的底层实现如下:

  1. class __lambda_9_14
  2.   {
  3.     public
  4.     inline /*constexpr */ void operator()() const
  5.     {
  6.       printf("%d, %d\n", x, ++y);
  7.     }
  8.     
  9.     private
  10.     int x;
  11.     int & y;
  12.     
  13.     public:
  14.     __lambda_9_14(int & _x, int & _y)
  15.     : x{_x}
  16.     , y{_y}
  17.     {}
  18.     
  19.   };

生成规则

看了前面的内容,lambda编译器的底层实现基本有了一个初步的认识,借助此文,将这个规则整理下:

编译器对lambda的生成规则如下:

  • lambda表达式中的捕获列表,对应lambda_xxxx类的private 成员

  • lambda表达式中的形参列表,对应lambda_xxxx类成员函数 operator()的形参列表

  • lambda表达式中的mutable,对应lambda_xxxx类成员函数 operator() 的常属性 const,即是否是常成员函数

  • lambda表达式中的返回类型,对应lambda_xxxx类成员函数 operator() 的返回类型

  • lambda表达式中的函数体,对应lambda_xxxx类成员函数 operator() 的函数体

效率

作为cpp开发人员,最关心的是性能问题。有些读者看完编译器对lambda的实现之后,感觉这么复杂的代码会不会效率很低?为了打消读者的疑虑,在本节中将从汇编角度进行分析。

我们以下面代码为例:

  1. int main() {
  2.   int x = 1;
  3.   auto fun = [x](){
  4.     printf("%d\n", x);
  5.   };
  6.   fun();
  7.   
  8.   return 0;
  9. }

使用-std=c++17 -stdlib=libc++ -O3优化之后,汇编代码如下:

  1. main: # @main
  2. push rax
  3. mov edi, offset .L.str
  4. mov esi, 1
  5. xor eax, eax
  6. call printf
  7. xor eax, eax
  8. pop rcx
  9. ret
  10. .L.str:
  11. .asciz "%d\n"

从上述汇编代码可以看出,经过编译器优化之后,效率非常高,所以我们上面的担心完全是多余的。

揭开lambda的神秘面纱

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/724222
推荐阅读
相关标签
  

闽ICP备14008679号