现代C++ | Lambda表达式+函数包装器相关特性
Lambda表达式
C++98/03 时代,如果你想给 std::sort、std::for_each、std::find_if 传一个自定义操作,只能写全局函数或写一个 functor 类,要定义 class、重载 operator(),写一堆无用代码。 C++委员会看到 Python、C#、Java 8 的匿名函数已经普及,就决定在 C++11 加 Lambda。 核心目标只有两个:就地写小函数,让代码逻辑更连贯 + 必须零开销,生成的机器码跟手写 functor 一模一样,不能变慢。这就是现代 C++“函数式编程”风格的起点。
Lambda表达式的语法结构:
[捕获列表](参数列表) mutable -> 返回类型 { 函数体 }
Lambda 根本不是函数,而是编译器在背后偷偷生成的一个独一无二的类并创建其对象,类型叫闭包类型 closure type,举个例子:
int factor = 10;
auto lam = [factor](int x) { return x * factor; };
编译器实际生成的代码大致是这样的:
struct __lambda_12345 { // 每个 lambda 都有唯一内部名字
int factor; // 捕获的变量变成成员
__lambda_12345(int f) : factor(f) {} // 构造函数
int operator()(int x) const { // 重载调用运算符
return x * factor;
}
};
auto lam = __lambda_12345(factor); // 实例化
-
捕获列表 [ ] 决定这个类有哪些成员变量 + 构造函数。
-
每个 lambda 在不同位置生成的类都不一样,所以必须用 auto 接收。
-
这就是“零开销”的秘密:最后汇编和自己手写一个 functor 类完全相同,不会多一条指令。
[捕获列表](参数列表) mutable -> 返回类型 { 函数体 }
什么时候必须用 -> 尾置返回类型?
-
C++11 环境下的复杂 Lambda:包含多个
return语句或返回类型不明显时。// C++11:如果有多个 return 或类型复杂,必须显式指定 auto func = [](int x) -> double { if (x > 0) { return x * 1.5; // 返回 double } else { return x * 2.0; // 也返回 double } }; -
需要显式指定返回类型的场景:即使编译器能推导,你也想强制指定返回类型(如强制返回
double而非float) -
C++14 增强了返回类型推导能力,普通 Lambda 直接用
auto即可
捕获列表:
-
[] 不捕获任何外部变量
-
[=] 值捕获所有(拷贝一份,安全)
-
[&] 引用捕获所有(最常用,性能最好)
-
[x] 只值捕获 x
-
[&x] 只引用捕获 x
-
[=, &x] 混合捕获(所有值捕获,但 x 用引用)
-
[this] 捕获当前对象的 this(类成员函数里常用)
-
C++17 :[self = *this](值捕获整个对象,避免悬挂引用)
mutable:值捕获默认是 const 的,加了才能在 lambda 里修改拷贝。因为 lambda 的 operator() 默认是 const。
C++11 的 Lambda 已经让就地定义小函数变得非常简洁,但捕获列表仍然存在局限性:只能直接捕获外部已有变量,无法在捕获列表中直接“初始化”一个新变量、进行类型转换、或者安全地移动资源(例如 std::move 一个 unique_ptr)。
C++14 引入初始化捕获(init-capture),目的是让 Lambda 捕获更加灵活、表达力更强,尤其在需要移动语义、创建临时对象、或者避免外部变量生命周期问题时,能写出更安全、更简洁的代码,同时保持零开销抽象。
Lambda 初始化捕获
C++14 允许在捕获列表中使用 var = expr 形式,在 Lambda 构造时直接初始化一个新成员变量,解决移动资源、临时对象、生命周期管理等实际痛点。
-
值捕获形式:[var = expr] → 拷贝/移动构造
-
引用捕获形式:[var = std::ref(expr)] → 引用
-
支持 std::move、std::make_unique 等任意表达式
// 安全移动 unique_ptr
///////////////////////////////////////////////////////
// C++11 老写法(必须用引用,生命周期容易出错)
std::unique_ptr<int> p = std::make_unique<int>(42);
auto lam = [&p]() { return *p; }; // p 必须活得比 lambda 长
// C++14 新写法(初始化捕获 + move)
auto lam = [ptr = std::move(p)]() mutable { // ptr 是 lambda 内部的新成员
return *ptr;
};
// p 现在已经为空,资源被安全移动进 lambda 内部
////////////////////////////////////////////////////////
// 在捕获列表中直接创建临时对象
////////////////////////////////////////////////////////
// C++11 老写法(必须在外部创建临时变量)
std::string temp = "hello";
auto lam = [temp = std::move(temp)]() { return temp + " world"; };
// C++14 新写法(直接在捕获列表初始化)
auto lam = [temp = std::string("hello")]() { return temp + " world"; };
///////////////////////////////////////////////////////
// 给捕获变量起新名字或进行转换
//////////////////////////////////////////////////////
int value = 42;
auto lam = [doubled = value * 2]() { return doubled; }; // doubled 是新名字,值为 84
// 更复杂转换
auto lam = [upper = std::string("hello")]() {
std::transform(upper.begin(), upper.end(), upper.begin(), ::toupper);
return upper;
};
/////////////////////////////////////////////////////
常见坑:
-
初始化捕获的变量只在 lambda 内部可见,外部无法访问。
-
使用 std::move 后,外部原变量进入“有效但未指定”状态,不能再使用。
-
如果 lambda 需要修改捕获的变量,必须加上 mutable 关键字(。
-
生命周期:初始化捕获的变量生命周期和 lambda 本身绑定,不会悬挂引用。
泛型 Lambda
C++14 允许 Lambda 的参数使用 auto,这让 Lambda 瞬间拥有了模板的能力:
// C++14:一个 Lambda 走天下
auto add = [](auto a, auto b) {
return a + b;
};
add(1, 2); // int
add(3.14, 2.71); // double
add(std::string("a"), "b"); // std::string
Lambda的坑:引用捕获后 lambda 活得比外部变量久 ,会导致悬挂引用
auto bad() {
int x = 42;
return [&]() { return x; }; // 返回后 x 已经销毁!
}
正确做法:用 [=] 值捕获,或 C++14 的 [x = x]
常见坑:
-
泛型 Lambda 的模板实例化是延迟的:泛型 Lambda 的
operator()模板只有在调用时才会实例化,若 Lambda 内部有语法错误,只要不调用就不会触发编译报错,调试时需注意。 -
显式模板参数必须在调用时指定:带模板参数列表的 Lambda,调用时必须显式指定模板参数,或让编译器完全推导,无法部分指定。
-
避免泛型 Lambda 过度使用导致的编译期膨胀:每一种不同的参数类型组合,都会生成一个新的模板特化版本,过度使用会导致编译期变长、目标文件体积变大。
-
Lambda 底层是怎么实现的?为什么零开销?
编译器生成一个临时类,重载 operator(),捕获变成成员变量,和手写 functor 完全一样。
-
[=] 和 [&] 区别?什么时候用哪个?
[=] 拷贝(安全但有拷贝开销),[&] 引用,但要注意生命周期。
-
C++14 初始化捕获和 C++11 普通捕获的本质区别是什么?
普通捕获是直接绑定外部已有变量;初始化捕获是在捕获列表里创建一个全新的成员变量并初始化,常用于移动资源或创建临时对象。
-
什么时候必须使用初始化捕获?
需要移动资源进 lambda(unique_ptr、string 等)、想在 lambda 内部创建一个独立副本、或者需要给捕获的变量起新名字/进行转换时。
-
初始化捕获的变量是值捕获还是引用捕获?
于你写的形式,[var = expr] 是值,[var = std::ref(expr)] 是引用。
-
C++14 泛型 lambda 有什么用?
可以写一个 lambda 支持任意类型(int、double、string 等)
一些建议:
-
短 lambda 用 [&]
-
要返回或传给别处的 lambda 用 [=]
-
类成员函数里捕获 *this 强烈推荐 [self = *this](C++17)
-
初始化捕获 + std::move 是 Lambda 中使用移动语义的标准写法,大量出现在回调函数、线程任务、STL 算法中
函数包装器相关特性
C++11 希望让函数式编程变得简单实用。Lambda 解决了“就地写小函数”的问题,但实际项目中经常需要存储、传递、复制一个“可调用对象”,比如 Lambda、普通函数、成员函数等。 为了解决这个问题,C++11 同时引入了 std::function、std::bind、std::ref、std::mem_fn 等工具,形成了一套完整的“函数对象包装”体系,让 Lambda 真正能被用在回调、事件、任务队列等场景中。
std::function
设计原因: 需要一种统一的、可存储、可复制的类型来保存任意可调用对象(Lambda、普通函数、成员函数、functor)。以前只能用函数指针,无法捕获状态或手写类,非常麻烦。
底层原理: std::function<R(Args...)> 是类型擦除的包装器。内部用虚表 + 小对象优化(,小对象直接放在栈上,大对象才分配堆内存。
代码示例:
#include <functional>
// 1. 存储 Lambda
std::function<void(int)> callback = [](int x) {
std::cout << "收到: " << x << '\n';
};
// 2. 存储普通函数
void my_func(int a) { ... }
std::function<void(int)> f = my_func;
// 3. 事件系统常用写法
class Button {
std::function<void()> on_click;
public:
void set_handler(std::function<void()> h) {
on_click = std::move(h); // 推荐用 move
}
void click() { if (on_click) on_click(); }
};
常见坑:
-
std::function 本身可拷贝,容易产生意外拷贝开销。
-
不要在性能热路径上大量使用,而是优先模板或直接 Lambda。
-
存储 Lambda 时一定要 std::move。
std::bind
设计原因: 想把函数的部分参数“绑定”成固定值,生成一个新的可调用对象,常用于适配旧接口。
底层原理:返回一个可调用对象,内部保存被绑定的参数和占位符(std::placeholders::_1 等)。
实际例子:
void handler(int a, std::string b, double c) { ... }
// 绑定后两个参数
auto bound = std::bind(handler, 42, std::placeholders::_1, 3.14);
bound("hello"); // 实际调用 handler(42, "hello", 3.14)
与 Lambda 对比,在项目中更推荐用 Lambda,std::bind 主要用于兼容旧代码或需要绑定成员函数时。
需要注意的是,哪么绑定的函数的参数是引用类型,std::bind 默认是 “值拷贝” 的。
#include <iostream>
#include <functional> // 要用到 std::bind
void add_one(int& n) { n++; }
int main() {
int x = 10;
// 不用 ref:bind 会偷偷把 x 拷贝一份,传给 add_one
auto func = std::bind(add_one, x);
func(); // 调用函数
std::cout << x << std::endl; // 输出 10!原变量没改!
}
std::ref / std::cref + std::reference_wrapper
设计原因:C++11 的 std::bind、std::function 以及 STL 算法,默认全是拷贝传参—— 你想传个引用进去改原对象?直接传 & 没用,它会拷贝成副本,再传引用进去。而std::ref 提供一种安全的引用包装,可以让 std::function 和 std::bind 正确处理引用。
std::reference_wrapper<T> = 能拷贝、能赋值的 “轻量引用壳子”
-
std::ref(x)→ 包普通左值引用T& -
std::cref(x)→ 包只读引用const T&
底层原理:
std::reference_wrapper<T> = 能拷贝、能赋值的 “轻量引用壳子”,是一个 **“引用的包装器”**,本质是一个类模板,里面存了一个指向原对象的指针。手动写 std::reference_wrapper<T> 太麻烦,C++11 提供了两个辅助函数:
-
std::ref(x):生成一个std::reference_wrapper<T>,包装x的非 const 引用; -
std::cref(x):生成一个std::reference_wrapper<const T>,包装x的const 引用。
实际例子:
#include <iostream>
#include <functional>
int main() {
int x = 10;
// 用 std::ref 包装 x,Lambda 里捕获的是 std::reference_wrapper
auto lambda = [x_ref = std::ref(x)]() mutable {
// x_ref 是 std::reference_wrapper<int>,可以隐式转换为 int&
x_ref.get() = 20; // 或者直接写 x_ref = 20(隐式转换)
std::cout << "Lambda 内部 x:" << x_ref << '\n';
};
lambda(); // 输出:Lambda 内部 x:20
std::cout << "外部 x:" << x << '\n'; // 输出:外部 x:20(原对象改了!)
// 用 std::function 存 Lambda,也能正确保持引用
std::function<void()> func = lambda;
func(); // 输出:Lambda 内部 x:20(继续修改原对象)
std::cout << "外部 x:" << x << '\n'; // 输出:外部 x:20
}
以std::bind部分的示例代码为例进行修改:
#include <iostream>
#include <functional>
// 一个接收引用参数的函数
void add_one(int& n) {
n++;
}
int main() {
int x = 10;
// 错误写法:不用 std::ref,bind 会拷贝 x,add_one 改的是副本
auto bad_bind = std::bind(add_one, x);
bad_bind();
std::cout << "x(不用 ref):" << x << '\n'; // 输出:10(没改!)
// 正确写法:用 std::ref 包装 x,bind 会传递引用
auto good_bind = std::bind(add_one, std::ref(x));
good_bind();
std::cout << "x(用 ref):" << x << '\n'; // 输出:11(改了!)
}
为什么改了?
std::ref(x) 会生成一个叫 std::reference_wrapper 的小东西 ,它本质是个 “壳子”,里面包着 x 的引用。std::bind 看到这个壳子,就知道 “哦,这是要传引用”,于是就不会拷贝 x 了,直接让 add_one 操作原变量。
| 场景 | 做法 |
|---|---|
给 std::bind 传普通引用参数 | 变量外面套 std::ref() |
给 std::bind 传只读引用参数 | 变量外面套 std::cref() |
| 其他时候(比如普通函数调用) | 直接用 & 就行,不用 ref |
需要注意的坑,则是生命周期。
std::ref 只是 “包装引用”,不会延长原对象的生命周期,如果原对象销毁了,std::reference_wrapper 也会变成悬空的,和引用捕获一样危险:
#include <iostream>
#include <functional>
std::function<void()> get_bad_func() {
int x = 10;
// 用 std::ref 包装局部变量 x,函数返回后 x 销毁
return std::bind([](int& n) { std::cout << n << '\n'; }, std::ref(x));
}
int main() {
auto bad_func = get_bad_func();
bad_func(); // 危险!访问悬空引用
}
所以被 std::ref 包装的对象,必须在使用它的 Lambda/function/bind 存在期间保持有效。
QA:
-
std::function 底层是怎么实现的?为什么有性能损失?
类型擦除 + 小对象优化。大对象需要堆分配和虚表调用。
-
什么时候必须用 std::function?
需要把可调用对象存起来、作为成员变量、传给回调、事件系统、任务队列等其他函数时。
-
std::bind 和 Lambda 哪个更好?
现代 C++ 推荐 Lambda,代码更清晰、可读性更高。std::bind 主要用于需要绑定成员函数或部分参数固定的场景。
- 为什么需要 std::ref?
让 std::function、std::bind 等能正确存储引用,而不是拷贝。
更多推荐
所有评论(0)