ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

【C++11】Lambda 表达式详细讲解(语法、捕获列表、mutable 关键字、返回值规则和常见避坑点)

【C++11】Lambda 表达式详细讲解(语法、捕获列表、mutable 关键字、返回值规则和常见避坑点)

文章目录

  • 前言
  • 一、Lambda 是什么?
  • 二、基本语法
    • 1. 完整格式
    • 2. 简单例子
  • 三、捕获列表(重点难点)
    • 1. 值捕获:拷贝变量
    • 2. 引用捕获:变量的别名
    • 3 隐式捕获:让编译器自己找变量
    • 4. 混合捕获:灵活搭配
    • 5. 捕获 this 指针
  • 四、mutable 关键字
  • 五、返回值规则
    • 1. 多个 return 且类型不一致
    • 2 空函数体或者复杂逻辑
  • 六、Lambda 的本质是什么?
  • 七、典型使用场景
    • 1. 配合 STL 算法
  • 八、常见坑点与避坑
    • 1. 悬空引用(重点):引用捕获的变量被销毁了
    • 2. 值捕获是「当时拷贝」
    • 3. 全局变量 / 静态变量不用捕获
    • 4. lambda 之间不能赋值
  • 总结

前言

在 C++11 之前,写回调、配合 STL 算法的时候,哪怕只有一两行逻辑,也得单独写个函数或者仿函数。名字想半天,定义还得跑到函数外面,代码拆得七零八落。

C++11 引入的lambda 表达式(也叫匿名函数)就是来解决这个问题的:就地定义、就地使用,短小的逻辑直接写在调用的地方,代码紧凑又好读。


一、Lambda 是什么?

lambda 本质就是一个没有名字的临时函数。用完就丢,不用额外起名字,也不用跑到外面定义。

举个最直观的对比:遍历数组打印每个元素。
不用 lambda 的写法:

#include<iostream>#include<vector>#include<algorithm>usingnamespacestd;voidPrint(intx){cout<<x<<" ";}intmain(){vector<int>v={1,2,3,4,5};// for_each:遍历迭代器区间内每一个元素,把每个元素传给可调用对象执行一遍for_each(v.begin(),v.end(),Print);return0;}

用 lambda 的写法:

#include<iostream>#include<vector>#include<algorithm>usingnamespacestd;intmain(){vector<int>v={1,2,3,4,5};// for_each:遍历迭代器区间内每一个元素,把每个元素传给可调用对象执行一遍for_each(v.begin(),v.end(),[](intx){cout<<x<<" ";});return0;}

逻辑直接写在调用的地方,不用单独定义函数,不用想函数名,阅读的时候一眼就能看到做了什么。

二、基本语法

1. 完整格式

[捕获列表](参数列表)mutable->返回值类型{函数体}

各个部分说明:

  • [捕获列表]:控制 lambda 能不能用外面的变量、怎么用。必须写,就算空的也要写[]。
  • (参数列表):和普通函数的形参一样。没有参数可以省略不写。
  • mutable:可选。值捕获的变量默认是 const 的,加了才能修改副本。
  • -> 返回值类型:可选。大部分情况编译器能自动推导,不用写。
  • {函数体}:逻辑代码。

2. 简单例子

1. 无参无返回值

autohello=[](){cout<<"hello lambda"<<endl;};hello();// 调用

2. 带参数

autoadd=[](inta,intb){returna+b;};cout<<add(1,2)<<endl;

3. 显式指定返回值

autofunc=[](intx)->double{if(x>0)returnx*1.5;elsereturnx*2.0;};

三、捕获列表(重点难点)

lambda 里想用外部作用域的变量,必须在捕获列表里声明,不然访问不到。这是 lambda 最核心也最容易搞混的部分。

1. 值捕获:拷贝变量

写法:[变量名],或者[=]隐式捕获所有用到的变量。

特点:

  • lambda 里得到的是副本,不是同一个变量
  • 默认是 const 的,不能修改(想改要加mutable,后面讲)
  • 捕获的时候就拷贝好了,后面外面变量再改变,lambda 里也不受影响
#include<iostream>usingnamespacestd;intmain(){inta=10;// 值捕获 aautofunc=[a](){cout<<a<<endl;// 读没问题//a = 20; // 编译报错:值捕获默认 const};func();// 输出 10a=20;func();// 还是输出 10,因为捕获的时候就拷贝了return0;}

2. 引用捕获:变量的别名

写法:[&变量名],或者[&]隐式捕获所有用到的变量。

特点:

  • lambda 里用的就是外面那个变量本身
  • 改了 lambda 里的值,外面也跟着变
  • 不存在 const 限制,该怎么改怎么改
#include<iostream>usingnamespacestd;intmain(){inta=10;// 引用捕获 aautofunc=[&a](){a=20;// 改变a的值};func();cout<<a<<endl;// 输出 20,外面也变了return0;}

3 隐式捕获:让编译器自己找变量

一个个写捕获变量太麻烦,可以让编译器根据 lambda 里用到的变量自动捕获:

  • [=]:所有用到的外部变量,全部值捕获
  • [&]:所有用到的外部变量,全部引用捕获
#include<iostream>usingnamespacestd;intmain(){inta=1,b=2,c=3;// 隐式值捕获autoadd1=[=](){returna+b+c;};// 隐式引用捕获autoadd2=[&](){a++;b++;c++;returna+b+c;};intresult1=add1();intresult2=add2();cout<<result1<<endl;cout<<result2<<endl;return0;}

4. 混合捕获:灵活搭配

允许大部分值捕获,个别变量单独引用捕获!
或者大部分引用捕获,个别变量单独值捕获!

#include<iostream>usingnamespacestd;intmain(){inta=1,b=2,c=3;// 整体值捕获,a 单独引用捕获autofunc1=[=,&a](){a=10;// a 是引用捕获,里面改变,外面也改变// b = 20; // b 是值捕获,const 改不了// c = 30; // c 是值捕获};// 整体引用捕获,b 单独值捕获autofunc2=[&,b](){a=10;// a 是引用捕获,里面改变,外面也改变//b = 20; // b 是值捕获,const 改不了c=30;// c 是引用捕获};return0;}

注意:隐式符号只能写一个,要么=要么&,剩下的单独指定相反的捕获方式。

5. 捕获 this 指针

在类的成员函数里写 lambda,想访问类的成员变量 / 成员函数,捕获this指针就行。

#include<iostream>usingnamespacestd;classTest{public:voidPrint(){// 捕获 this,就能用 _numautofunc=[this](){cout<<_num<<endl;};func();}private:int_num=100;};intmain(){Test test;test.Print();return0;}

四、mutable 关键字

值捕获的变量默认是 const 的,想修改这个副本,需要加mutable。

#include<iostream>usingnamespacestd;intmain(){intcnt=0;// 加 mutable 才能修改值捕获的 cntautoinc=[cnt]()mutable{cnt++;cout<<cnt<<endl;};inc();// 1inc();// 2cout<<cnt<<endl;// 外面还是 0,改的是副本return0;}

注意:mutable 改的只是 lambda 内部的副本,外面的变量不受影响。

为什么第二次inc();的结果是2?

原因:
lambda 对象 inc 创建后,值捕获的 cnt 被拷贝到 lambda 内部,成为 lambda 的成员变量;内部的cnt副本会保留状态,之后每次调用 inc(),都是复用同一个 inc 对象,操作它里面保存的那份副本,不会再次从外部拷贝。

五、返回值规则

lambda 的返回值大部分时候不用写,编译器自动推导。但有两种情况建议显式写。

1. 多个 return 且类型不一致

如果函数体里有多个 return,返回的类型不一样,编译器可能推不出来,或者推出来的不是你想要的。显式指定更稳妥。

autofunc=[](intx)->double{if(x>0)returnx*1.5;// doubleelsereturnx;// int};

2 空函数体或者复杂逻辑

有时候逻辑复杂,编译器不好推导,显式写-> 返回类型也更清晰。

六、Lambda 的本质是什么?

lambda 不复杂,编译器看到 lambda,就会自动生成一个匿名的仿函数类:

  • 捕获的变量变成类的成员变量
  • 重载operator(),函数体就是 operator () 的实现

所以:

  1. 每个 lambda 的类型都是唯一的,哪怕两个 lambda 长得一模一样,类型也不同。所以不能互相赋值。
  2. 值捕获默认 const,因为生成的 operator () 是 const 成员函数。加了 mutable 就变成非 const 的。
  3. lambda 的大小,就是捕获变量的大小之和。
#include<iostream>usingnamespacestd;intmain(){inta=0;doubleb=0;autofunc=[a,b](){};cout<<sizeof(func)<<endl;// 16,就是 int + double 的大小return0;}

想要把这个 lambda 存储起来怎么办?需要用包装器std::function

#include<iostream>#include<functional>usingnamespacestd;function<int(int,int)>add=[](inta,intb){returna+b;};intmain(){intret=add(1,2);cout<<ret<<endl;return0;}

七、典型使用场景

1. 配合 STL 算法

这是 lambda 最常用的地方,自定义排序、查找条件、遍历操作,都特别方便。

自定义排序

#include<iostream>#include<vector>#include<algorithm>usingnamespacestd;intmain(){vector<int>v={3,1,4,2,5};// 降序排序sort(v.begin(),v.end(),[](inta,intb){returna>b;});for(auto&e:v){cout<<e<<" ";}cout<<endl;return0;}

条件查找

#include<iostream>#include<vector>#include<algorithm>usingnamespacestd;intmain(){vector<int>v={3,1,4,2,5};// 找第一个大于 3 的数// find_if:遍历迭代器区间元素,依次把元素传给可调用对象;遇到第一个返回 true 的元素,就返回;否则返回 end()autoit=find_if(v.begin(),v.end(),[](intx){returnx>3;});cout<<*it<<endl;// 4return0;}

遍历操作

#include<iostream>#include<vector>#include<algorithm>usingnamespacestd;intmain(){vector<int>v={3,1,4,2,5};// for_each:遍历迭代器区间内每一个元素,把每个元素传给你给的可调用对象执行一遍for_each(v.begin(),v.end(),[](int&x){x*=2;// 每个元素乘 2});for(auto&e:v){cout<<e<<" ";}cout<<endl;return0;}

八、常见坑点与避坑

1. 悬空引用(重点):引用捕获的变量被销毁了

引用捕获的是局部变量,函数结束变量销毁了,再调用 lambda 就会访问非法内存。

反面例子:

#include<iostream>usingnamespacestd;autoGetLambda(){intx=10;return[&x](){// 捕获局部变量的引用cout<<x<<endl;};}intmain(){autofunc=GetLambda();func();// 运行崩溃或者未定义行为!因为 x 已经销毁了return0;}

能值捕获就别引用捕获;必须引用捕获的,一定要保证变量生命周期比 lambda 长。

2. 值捕获是「当时拷贝」

捕获那一刻就拷贝好了,后面外面变量再变,lambda 里的不会跟着变。

#include<iostream>usingnamespacestd;intmain(){inta=10;autofunc=[a](){cout<<a<<endl;};a=20;func();// 输出 10,不是 20return0;}

3. 全局变量 / 静态变量不用捕获

全局变量、静态变量不在栈上,lambda 直接就能用,不用捕获。

4. lambda 之间不能赋值

每个 lambda 类型都是编译器生成的唯一类型,两个 lambda 不能互相赋值。想统一存储用包装器std::function。


总结

  1. lambda 是匿名函数,就地定义就地用,适合短小逻辑。
  2. 捕获列表控制外部变量的访问方式:值捕获是副本,引用捕获是别名。
  3. mutable用来修改值捕获的副本,不影响外部变量。
  4. 返回值一般自动推导,多 return 且类型不同时就建议显式指定。
  5. 本质是编译器生成的匿名仿函数类,每个 lambda 类型唯一。
  6. 坑点:悬空引用、值捕获时机、类型不可赋值。

lambda 是 C++11 非常实用的特性,写算法、写回调、封装逻辑都离不开。把上面这些点搞懂,日常使用就基本没问题了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进