C++11 工程级应用 08:Lambda表达式与Tuple元组
- [一、Lambda 表达式:就地诞生的匿名闭包😎](#一、Lambda 表达式:就地诞生的匿名闭包😎)
-
- [✨Lambda 的核心优势](#✨Lambda 的核心优势)
- 📝基础语法拆解
- 🎯捕获列表:掌控外部变量的访问权限
- [类内部 Lambda 捕获示例](#类内部 Lambda 捕获示例)
- 🧩mutable:修改值捕获的副本
- [📌Lambda 的类型特性](#📌Lambda 的类型特性)
- 🚀实战:替代老旧仿函数,简化算法调用
- [二、Tuple 元组:轻量万能 "匿名结构体"📦](#二、Tuple 元组:轻量万能 “匿名结构体”📦)
-
- 基础构造与取值
- [tie 解包 tuple,std::ignore 占位忽略元素](#tie 解包 tuple,std::ignore 占位忽略元素)
- [tuple_cat 拼接多个元组](#tuple_cat 拼接多个元组)
- 🎯总结
导读:C++11 作为 C++ 发展史上里程碑式的版本,带来大量颠覆传统编码习惯的特性。很多同学写 C++ 还在手写笨重仿函数、定义大量临时结构体,今天就聊聊两大神器:Lambda 表达式与 Tuple 元组,帮你把代码写得更优雅、更简洁。
在 C++11 诞生之前,想要给标准库算法传递一段自定义逻辑,我们往往要大费周章。如果想要组合多个判断条件,还得依靠std::bind层层嵌套,代码读起来绕来绕去,维护成本很高。而 C++11 带来的 Lambda 表达式,把函数式编程思想带入 C++ 世界;配套的 Tuple 元组,则给我们提供了轻量 "匿名结构体" 的能力。两者搭配,直接改写日常编码的体验。
一、Lambda 表达式:就地诞生的匿名闭包😎
Lambda 来源于函数式编程,简单来说,它就是可以就地定义的匿名函数,能够捕获周边作用域的变量,形成闭包。早在 C#3.5 就已经出现 Lambda,Java 直到 Java8 才正式支持,而 C++11 补齐了这块短板。
✨Lambda 的核心优势
-
声明式编程,逻辑就地书写:不用单独定义全局函数、仿函数类,业务逻辑和调用代码写在一起,阅读代码不用来回跳转文件。
-
代码精简,拒绝模板膨胀:省去大量冗余的函数对象样板代码,开发者聚焦业务逻辑本身,开发效率直接拉满。
-
灵活闭包能力:运行时捕获上下文变量,做到随用随定义,程序逻辑更加灵活。
📝基础语法拆解
完整 Lambda 语法格式:
cpp
[捕获列表](参数列表)opt -> 返回类型 {函数体};
-
[]:捕获列表,控制可以访问哪些外部变量; -
():参数列表,和普通函数参数一致; -
-> ret:后置返回类型,大部分场景可以省略,编译器自动推导; -
{}:函数体,业务逻辑写在这里。
简单示例:
cpp
//显式指定返回值
auto f = [](int a)->int{ return a + 1; };
std::cout << f(1) << std::endl; //输出2
//省略返回值,编译器自动推导
auto f2 = [](int a){ return a + 1; };
⚠️注意:返回值自动推导有坑!如果返回初始化列表
{...},编译器无法推导类型,必须手动写明返回类型。
cpp
auto x1 = [](int i){ return i; }; //OK,推导返回int
auto x2 = [](){ return {1,2}; }; //编译报错!初始化列表无法自动推导
当 Lambda 没有参数时,参数列表的小括号也可以直接省略,下面两种写法等价:
cpp
auto f1 = [](){ return 1; };
auto f2 = []{ return 1; }; //省略空参数列表
🎯捕获列表:掌控外部变量的访问权限
捕获列表是 Lambda 的灵魂,决定 Lambda 可以拿到哪些外部变量,是拷贝访问还是引用访问。
| 捕获写法 | 作用说明 |
|---|---|
[] |
什么都不捕获,无法访问外部变量 |
[&] |
全部外部变量按引用捕获,可以修改外部原值 |
[=] |
全部外部变量按值捕获,拷贝一份副本,默认不能修改副本 |
[=,&foo] |
全部变量值捕获,唯独 foo 变量使用引用捕获 |
[bar] |
仅值捕获 bar 这一个变量 |
[this] |
捕获类的 this 指针,可以访问类成员变量、成员函数 |
类内部 Lambda 捕获示例
cpp
#include <iostream>
class A
{
public:
int i_ = 0;
void func(int x, int y)
{
// auto x1 = []{ return i_; }; //错误,没有捕获任何变量,访问不到i_
auto x2 = [=]{ return i_ + x + y; }; //OK,值捕获全部外部变量
auto x3 = [&]{ return i_ + x + y; }; //OK,引用捕获全部外部变量
auto x4 = [this]{ return i_; }; //OK,捕获this,访问成员变量
// auto x5 = [this]{ return i_ + x + y; }; //错误,只捕获this,x、y没有捕获
auto x6 = [this, x, y]{ return i_ + x + y; }; //OK,同时捕获this、x、y
auto x7 = [this]{ return i_++; }; //OK,通过this修改类成员
}
};
普通作用域下捕获:
cpp
int a = 0, b = 1;
// auto f1 = []{ return a; }; //错误,未捕获变量
auto f2 = [&]{ return a++; }; //OK,引用捕获,可以修改外部a
auto f3 = [=]{ return a; }; //OK,值捕获,读取a副本
// auto f4 = [=]{ return a++; }; //错误!值捕获的副本默认不能修改
// auto f5 = [a]{ return a + b; }; //错误,只捕获a,b未捕获
auto f6 = [a, &b]{ return a + (b++); };//OK,a值捕获,b引用捕获
auto f7 = [=, &b]{ return a + (b++); };//OK,全部值捕获,b单独引用捕获
💡重点坑点:值捕获是在 Lambda 定义瞬间拷贝变量,不是调用的时候
cpp
int a = 0;
auto f = [=]{ return a; }; //定义时拷贝a=0保存到闭包内部
a += 1;
std::cout << f() << std::endl; //输出0,不是1!
哪怕外部后续修改 a,闭包内部存的还是定义时刻的副本。如果想要每次调用都拿到外部最新值,请使用引用捕获[&]。
🧩mutable:修改值捕获的副本
默认情况下,Lambda 的operator()是 const 修饰的成员函数,值捕获进来的副本不允许修改。如果想要修改闭包内部拷贝出来的副本,需要加上mutable关键字。
注意:使用 mutable,哪怕没有参数,也不能省略小括号
()!
cpp
int a = 0;
// auto f1 = [=]{ return a++; }; //编译报错,const,不能修改副本
auto f2 = [=]() mutable { return a++; }; //OK,mutable去掉const限制
mutable 修改的仅仅是闭包内部的副本,不会影响外部原始变量。
📌Lambda 的类型特性
Lambda 的类型叫做闭包类型(Closure Type),本质是编译器自动生成的匿名仿函数类。
-
可以用
std::function来保存 Lambda 对象; -
无任何捕获的 Lambda,可以直接转换为普通函数指针;
-
只要有捕获变量,就不能转为普通函数指针。
cpp
#include <functional>
//用std::function存储lambda
std::function<int(int)> f1 = [](int a){ return a; };
//无捕获,可以转函数指针
using FuncPtr = int(*)(int);
FuncPtr fp = [](int a){return a;};
fp(123);
typedef void(*Ptr)(int*);
Ptr p = [](int* p){ delete p; }; //无捕获,编译通过
//下面这行如果加上捕获变量,就编译失败
//Ptr p2 = [&](int* p){ delete p; }; //有捕获,无法转函数指针
🚀实战:替代老旧仿函数,简化算法调用
C++11 之前,使用std::for_each统计 vector 偶数数量,需要手写完整仿函数类,样板代码一大堆:
cpp
#include <vector>
#include <algorithm>
class CountEven
{
int& count_;
public:
CountEven(int& count):count_(count){}
void operator()(int val)
{
if(!(val & 1))
{
++count_;
}
}
};
int main()
{
std::vector<int> v = {1,2,3,4,5,6};
int even_count = 0;
std::for_each(v.begin(),v.end(),CountEven(even_count));
std::cout << "偶数数量:" << even_count << std::endl;
return 0;
}
有了 Lambda 之后,逻辑直接写在调用处,不再需要单独写类:
cpp
std::vector<int> v = {1,2,3,4,5,6};
int even_count = 0;
std::for_each(v.begin(),v.end(),[&even_count](int val){
if(!(val & 1))
{
++even_count;
}
});
std::cout << "偶数数量:" << even_count << std::endl;
还记得早期用std::bind拼接多条件判断的写法吗?筛选容器中大于 5、小于 10 的元素:
cpp
//bind嵌套写法,代码冗长晦涩
using std::placeholders::_1;
auto f = std::bind(std::logical_and<bool>(),
std::bind(std::greater<int>(),_1,5),
std::bind(std::less_equal<int>(),_1,10));
int count = std::count_if(coll.begin(),coll.end(),f);
换成 Lambda,逻辑一目了然,可读性直接碾压 bind 版本:
cpp
int count = std::count_if(coll.begin(),coll.end(),[](int x){
return x>5 && x<=10;
});
后续修改条件也极其方便,改一行布尔表达式即可。
小提示:Lambda 绝大多数场景可以替代
std::function,但部分老旧第三方库不支持 Lambda,此时仍然需要std::function做一层包装。
二、Tuple 元组:轻量万能 "匿名结构体"📦
std::tuple是固定大小、可以存放不同类型元素的集合,可以理解为泛化版的 std::pair。很多时候我们仅仅需要临时打包一组异构数据,不想专门定义一个 struct 结构体,这时 tuple 就派上用场。
基础构造与取值
cpp
#include <tuple>
//构造tuple,等价于手写一个struct{const char* p; int len;};
std::tuple<const char*, int> tp = std::make_tuple("hello", 5);
//get<下标>()获取元素,下标编译期确定,从0开始
const char* data = std::get<0>(tp);
int len = std::get<1>(tp);
tie 解包 tuple,std::ignore 占位忽略元素
std::tie可以把 tuple 里面的值直接解包赋值给普通变量。如果不需要某一个位置的数据,可以用std::ignore占位跳过。
cpp
std::tuple<int,std::string,double> t = {100,"c++11",3.14};
int x;
std::string s;
double d;
//全部解包
std::tie(x,s,d) = t;
//只取第三个元素,前两个直接忽略
std::tie(std::ignore, std::ignore, d) = t;
补充:
std::tie生成的是引用元组;std::forward_as_tuple会生成右值引用元组,非常适合容器 emplace 原位构造,减少对象拷贝:
cpp
std::map<int,std::string> m;
m.emplace(std::forward_as_tuple(10, std::string(20,'a')));
tuple_cat 拼接多个元组
std::tuple_cat可以把多个 tuple、pair 拼接成为一个更大的 tuple:
cpp
#include <iostream>
#include <tuple>
#include <string>
void print(auto t)
{
//简单打印tuple,演示效果
std::cout << std::get<0>(t) << ","
<< std::get<1>(t) << ","
<< std::get<2>(t) << ","
<< std::get<3>(t) << "n";
}
int main()
{
std::tuple<int, std::string, float> t1(10,"Test",3.14);
int n = 7;
auto t2 = std::tuple_cat(t1, std::make_pair("Foo","bar"),t1,std::tie(n));
n = 10;
print(t2);
return 0;
}
拼接之后,把多个异构容器合并,适合函数返回多组不同类型返回值,省去自定义结构体。
🎯总结
-
Lambda 表达式给 C++ 带来闭包能力,告别笨重仿函数,就地书写业务逻辑。重点掌握捕获列表、mutable、值捕获与引用捕获的区别;值捕获拷贝发生在定义时刻,不是调用时刻,这个高频坑一定要记牢。无捕获 Lambda 可以转函数指针,带捕获不行。
-
Tuple 元组充当轻量匿名结构体,适合临时打包多类型数据,
std::get取值、std::tie解包、tuple_cat拼接,快速完成多值传递,减少大量结构体定义。

C++11 这些特性不是语法糖那么简单,它们实实在在降低编码复杂度,让代码既简短又好维护。把 Lambda 和 tuple 用好,写出来的 C++ 代码会完全是另一种风格。