C++填坑系列——lambda表达式

lambda 表达式

总结:

  1. lambda表达式原理:被编译器转换为类+初始化对象的代码;
  2. 格式:[captures list] (params list) specifiers exception -> retType {funtion body}
  3. 按值捕获和按引用捕获的优缺点以及解决方法;

一. lambda原理

lambda函数会被编译器转换为,并定义实现一个operator()

https://cppinsights.io/这个网站里我们看下,

顺便推荐下这个网站 cppinsights
能看到你写的代码经过编译器处理之后的代码样式,非常好用(^ω^)

#include <cstdio>

int main()
{
    int value = 10;
    auto func = [value](int a, int b) {
      printf("%d\n", value); 
      return a + b;
    };
  
    func(5, 6);
}

经过编译器处理之后会变成如下代码:

#include <cstdio>

int main()
{
  int value = 10;
    
  class __lambda_6_17
  {
    public: 
    inline /*constexpr */ int operator()(int a, int b) const
    {
      printf("%d\n", value);
      return a + b;
    }
    
    private: 
    int value;
    
    public:
    __lambda_6_17(int & _value)
    : value{_value}
    {}
    
  };
  
  __lambda_6_17 func = __lambda_6_17{value};
  func.operator()(5, 6);
  return 0;
}

通过上面的两段代码对比,其实可以看到lambda函数func会被编译器转换为class __lambda_6_17,并且这个类重载了函数调用运算符operator()

其中就是c++中仿函数的概念
而重载operator()就可以让一个类型像函数那样调用,类的行为和函数相似
像上面的类__lambda_6_17,其实这样调用__lambda_6_17();就会自动调用内部的operator()

二. lambda expression格式

[captures list] (params list) specifiers exception -> retType {funtion body}
  1. specifiers: optional可选限定符,默认是const,可选mutable;

    • 这个const有什么用呢?
    • int operator()(int a, int b) const
    • 就是上面这个const,表示函数体内不能修改捕获的变量;如果是mutable,那可以在函数体内修改捕获变量的值。
  2. -> retType: optional返回值类型,大多数情况下返回类型都可以自动推导出来;有些场景不行;

  3. (params list): optional参数列表,可以不写参数;

  4. [captures list]:

    • 捕获列表:有按值捕获和按引用捕获,捕获的变量都会成为类内的成员变量;不需要捕获静态变量和全局变量(没必要转换为类内的成员变量);
    • 捕获时机:捕获发生在lambda定义的时候,而非调用的时候,所以在定义之后更改了捕获变量的值,其实这个lambda内部使用的还是lambda定义之前的变量的值;
    • 初始化捕获;

(1) 捕获列表

  1. 按值捕获的问题
  • 捕获的是变量副本,而非变量本身;这就导致一些不可复制的对象不能被捕获,如:unique_ptr
  • 捕获时机在lambda创建时,后续变量被更新,lambda内部的变量也不会再发生变化;
  • 捕获变量是比较大的对象,会有较大的资源消耗。
  1. 按引用捕获的问题
  • 捕获时会创建一个对外部变量的引用,如果外部变量的声明周期结束了,但是lambda内部还在使用就会出现问题;

来个例子,如何实现?主要是能够让lambda捕获的变量和lambda的声明周期不一致即可:

#include <cstdio>

auto func() {
  int value = 10;
  auto lambda_func = [&value]() { printf("%d\n", value); };
  return lambda_func;
}

int main() {
  auto func_var = func();  // 这里延长了lambda的声明周期
  func_var();
}

(2) 捕获时机:

下面的代码证明了捕获的时机发生在lambda函数定义的时候,下面的valuefunc调用之前再次修改了值,但是函数输出的结果依旧是10。

注意

  1. 对于按值捕获来说,定义auto func = [value](int a, int b) {}的这段代码被编译器转换成了两部分:class __lambda_5_15 {};__lambda_5_15 func = __lambda_5_15{value};。其实在定义之后,直接就用捕获的变量的值调用了构造函数。
  2. 对于按引用捕获来说,类内的成员变量是一个引用变量,所以它的值是会随着捕获变量更新而改变的。
#include <cstdio>

int main() {
  int value = 10;
  auto func = [value](int a, int b) {
    printf("%d\n", value);
    return a + b;
  };
  value = 20;
  func(5, 6);  // output: 10
}
#include <cstdio>

int main()
{
  int value = 10;
    
  class __lambda_5_15
  {
    public: 
    inline /*constexpr */ int operator()(int a, int b) const
    {
      printf("%d\n", value);
      return a + b;
    }
    
    private: 
    int value;
    
    public:
    __lambda_5_15(int & _value)
    : value{_value}
    {}
    
  };
  
  __lambda_5_15 func = __lambda_5_15{value};  // 这里在定义class之后,直接调用了构造函数
  value = 20;
  func.operator()(5, 6);
  return 0;
}

(3)初始化捕获

lambda对外部进行捕获时,实际执行的是赋值初始化的操作:

auto func1 = [value]() {} 等价于 auto func1 = [value = value]() {}

这样的话,对于那些不可复制的对象或者比较大的对象来说,就可以使用移动语义了:

auto func3 = [unique_value_in = std::move(unique_value)]() {}

具体的就是下面这段代码:

#include <cstdio>
#include <memory>
#include <utility>

int main() {
  int value = 10;
  std::unique_ptr<int> unique_value = std::make_unique<int>(5);

  auto func1 = [value = value]() { printf("%d\n", value); };
  auto func2 = [v = value]() { printf("%d\n", v); };

  // auto func3 = [unique_value]() { printf("%d\n", *unique_value); };
  auto func3 = [unique_value_in = std::move(unique_value)]() {
    printf("%d\n", *unique_value_in);
  };

  func1();
  func2();
  func3();
}
posted @ 2024-03-02 12:27  战斗天使zzy  阅读(98)  评论(0)    收藏  举报