首页 > 编程语言 >C++填坑系列——lambda表达式

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

时间:2024-03-02 12:33:21浏览次数:28  
标签:__ int 捕获 value 填坑 C++ func 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();
}

标签:__,int,捕获,value,填坑,C++,func,lambda
From: https://www.cnblogs.com/pplearn/p/18048487

相关文章

  • C++ 类访问修饰符
    私有(private)成员成员和类的默认访问修饰符是private,如果没有使用任何访问修饰符,类的成员将被假定为私有成员。私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员。实际操作中,我们一般会在私有区域定义数据,在公有区域定义相关的函数......
  • C++类开发的第六篇(虚拟继承实现原理和cl命令的使用的bug修复)
    Class_memory接上一篇末尾虚拟继承的简单介绍之后,这篇来详细讲一下这个内存大小是怎么分配的。使用clcl是MicrosoftVisualStudio中的C/C++编译器命令。通过在命令行中键入cl命令,可以调用VisualStudio的编译器进行编译操作。cl命令提供了各种选项和参数,用于指定源......
  • C++ 把引用作为返回值
    通过使用引用来替代指针,会使C++程序更容易阅读和维护。C++函数可以返回一个引用,方式与返回一个指针类似。当函数返回一个引用时,则返回一个指向返回值的隐式指针。这样,函数就可以放在赋值语句的左边。例如,请看下面这个简单的程序:1#include<iostream>23usingnamesp......
  • C++ 类的内存布局
    基类类内成员的内存分布常见类内成员大致分为:类内变量、类内函数、静态变量、虚函数等,内存分布遵循:所有成员会按照声明的顺序布局类内成员会进行大对齐类内函数不占用类的内存,存储在代码区静态变量不占用类的内存,存储在全局/静态区所有虚函数共用一个虚函数表指针,虚函数表......
  • C++ 把引用作为参数
    1#include<iostream>2usingnamespacestd;34//函数声明5voidswap(int&x,int&y);67intmain()8{9//局部变量声明10inta=100;11intb=200;1213cout<<"交换前,a的值:"<<a<<en......
  • C++ 从函数返回指针
    C++允许从函数返回指针,必须声明一个返回指针的函数:int*myFunction()C++不支持在函数外返回局部变量的地址,除非定义局部变量为static变量。1#include<iostream>2#include<ctime>3#include<cstdlib>4 5usingnamespacestd;6 7//要生成和返回随......
  • C++ 指针 vs 数组
    指针和数组并不是完全互换的1#include<iostream>23usingnamespacestd;4constintMAX=3;56intmain()7{8intvar[MAX]={10,100,200};910for(inti=0;i<MAX;i++)11{12*var=i;//这是正确的语法13......
  • Lambda实现条件去重distinct List
    原文链接:https://blog.csdn.net/qq_39940205/article/details/114269686  _______________________________________________________________________________________________________________我们知道,Java8lambda自带的去重为distinct方法,但是只能过滤整体对象,不......
  • C++类开发第五篇(继承和派生的初体验)
    inheritance在C++中,继承是一种面向对象编程的特性,允许一个类(称为子类或派生类)从另一个类(称为基类或父类)那里继承属性和行为。通过继承,子类可以获得父类的数据成员和成员函数,从而可以重用父类的代码并扩展其功能。这样可以提高代码的复用性和可维护性,同时也符合面向对象编程的封......
  • C++static 存储类
    1#include<iostream>23//函数声明4voidfunc(void);56intmain()7{8intcount=10;9while(count--)10{11func();12std::cout<<",变量count为"<<count<<std::endl;13......