Lambda 表达式(也称为匿名函数)是 C++11 引入的一项核心特性,它允许我们在函数内部定义临时的、可调用的对象,而无需显式定义一个仿函数类。
顺带着也极大地简化了回调、STL 算法谓词、并发任务等场景的代码编写,使代码更紧凑、更易读。
今天,我们就来细品这颗“语法糖”,看看它为什么这么甜,以及怎么吃才不会蛀牙(避免陷阱)。
函数指针
你有没有想过,在lambda表达式出现之前C++ 程序员都是用什么方法实现类似的效果呢?
没错,第一种方法就是使用传统的函数指针。
#include <iostream>
// 声明函数指针
int (*funcPtr) (int, int);
int add(int a, int b)
{
return a + b;
}
int main()
{
funcPtr = &add; // 给函数指针赋值
int res = funcPtr(3, 5); // 调用
std::cout << res << std::endl; // 输出:8
return 0;
}
上述代码又是声明,又是赋值,又是调用的,使用不够灵活而且过于繁琐。
函数指针的局限性:
- 无法捕获上下文变量(即不能有状态)。
- 只能指向全局函数或静态成员函数。
- 类型安全性较弱(函数指针类型需严格匹配)。
- 不支持泛型算法中的内联优化(编译器难以内联)。
仿函数
仿函数(Functor),又称函数对象(Function Object),是指重载了 operator() 的类对象。
它可以像普通函数一样被调用,但比普通函数更灵活,因为它可以拥有自己的状态(成员变量),并且可以被内联优化,效率更高。
基本实现
定义一个类,重载 operator(),然后创建该类的对象,使用 对象名(参数) 的方式调用。
#include <iostream>
class Adder {
public:
// 重载函数调用运算符
int operator()(int a, int b) const
{
return a + b;
}
};
int main() {
Adder add; // 创建一个仿函数对象
int result = add(3, 4); // 像函数一样使用
std::cout << result << std::endl; // 输出 7
}
优势
可以携带状态
普通函数只能通过静态局部变量或全局变量来保存状态,容易引起线程安全问题。
而仿函数可以将状态封装在成员变量中,使其在调用时具备上下文信息。
class Accumulator {
public:
Accumulator(int init = 0) : total(init) {}
int operator()(int value)
{
total += value;
return total;
}
int getTotal() const { return total; }
private:
int total;
};
int main() {
Accumulator acc(10);
std::cout << acc(5) << std::endl; // 输出 15
std::cout << acc(3) << std::endl; // 输出 18
}
我们可以看到通过存储total这个成员变量,每次调用不会被初始化,能够不断地累加下去。
可作为模版参数
仿函数通常与 STL 算法结合使用,作为模板参数传递,例如 std::sort 的第三个参数。
#include <iostream>
#include <algorithm>
#include <vector>
struct Greater
{
bool operator()(int a, int b) const
{
return a > b;
}
};
int main() {
std::vector<int> v = { 3, 1, 4, 1, 5 };
std::sort(v.begin(), v.end(), Greater()); // 降序排序
return 0;
}
支持内联优化
由于仿函数是一个具体的类型,编译器可以轻松地将 operator() 内联展开,避免了函数指针调用时的间接跳转开销。
缺点
- 需要单独定义一个类,代码冗长。
- 如果要在多个地方使用不同状态,需要定义多个仿函数类,或使用模板参数传入状态。
- 捕获多个变量时,类的成员会增多,书写繁琐。
lambda表达式
通过以上的两种方法,我们可以看到它们虽然能够完成任务,但往往代码冗长、可读性差,且使用不够灵活。
而Lambda表达式整合了上述方法的优点,同时克服了它们的缺点:
- 简洁的语法:捕获列表 -> 返回类型 { 函数体 },无需单独定义类。
- 方便的变量捕获:按值、按引用捕获上下文变量,甚至能捕获 this。
- 性能:编译器通常生成与手写仿函数同样高效的代码,且容易内联。
- 与标准库无缝集成:所有接受可调用对象的算法都能直接使用 Lambda。
基本语法
[captures_list](parameters_list) -> return_type { function_body }
- captures_list:捕获列表定义了如何访问其所在作用域中的局部变量(包括 this)。捕获方式分为值捕获、引用捕获和隐式捕获。
- parameters_list:参数列表定义了lambda接受哪些参数。
- return_type:返回类型,通常可以省略,由编译器根据自动推导。
- function_body:函数体,包含了lambda执行的代码。
捕获方式
- 值捕获:捕获外部变量的副本。
int x = 10;
auto lambda = [x]() ->void {
std::cout << x << std::endl;
}; // 捕获x的副本
x = 20;
lambda(); // 输出:10,因为捕获的是x的副本
- 引用捕获:捕获外部变量的引用。
int x = 10;
auto lambda = [&x]() ->void {
std::cout << x << std::endl;
}; // 捕获x的引用
x = 20;
lambda(); // 输出:20,引用了外部的 x
- 隐式捕获:使用[=]或[&],表示按值或引用捕获所有外部变量的副本或引用。
(不推荐使用隐式捕获,隐式捕获会造成不必要开销影响性能,并且代码可读性下降以及线程安全问题) - this捕获:在类的非静态成员函数中,可以使用[this] 捕获当前对象指针,从而访问成员变量和成员函数。
class A {
public:
void func() {
auto lambda = [this]() {
std::cout << this->value << std::endl;
};
lambda();
}
private:
int value = 10;
};
mutable关键字
默认情况下,lambda 的 operator() 是 const 的,意味着它不能修改按值捕获的变量。
若需要修改按值捕获的变量,我们可以使用 mutable 关键字。
int main() {
int count = 0;
auto lambda = [count]() mutable{
++count;
std::cout << "count在lambda内部的值: " << count << std::endl;
};
lambda();
lambda();
std::cout << "count在lambda外部的值: " << count << std::endl;
return 0;
}
程序输出:
count在lambda内部的值: 1
count在lambda内部的值: 2
count在lambda外部的值: 0
上述代码中我们调用了两次lambda()函数,可以看到通过mutable关键字成功修改了lambda捕获的count值,但外部的count值没有改变。
lambda与标准库算法
许多标准库算法接受可调用对象,可以直接传入 lambda,让代码更简洁和直观。
#include <iostream>
#include <algorithm>
#include <vector>
int main() {
std::vector<int> v = { 1,3,5,2,7 };
std::sort(v.begin(), v.end(),
[](int a, int b) {
return a < b; // 升序
}
);
for (auto v1 : v)
std::cout << v1 << " ";
std::cout << std::endl;
return 0;
}
程序输出:
1 2 3 5 7
lambda与智能指针的结合
关于智能指针可以查看我的另一篇博客:智能指针
在现代 C++ 异步编程中,我们经常需要将回调函数(如 lambda)传递给异步任务(如线程、std::async、网络 I/O 回调等)。
这些回调往往在将来的某个时刻执行,而此时原始对象可能已经超出作用域被销毁,导致悬挂指针和未定义行为。
使用 std::shared_ptr 结合 lambda 可以优雅地解决这一问题:
通过将 shared_ptr 按值捕获到 lambda 中,增加对象的引用计数,从而延长其生命周期,直到 lambda 执行完毕。
我们先来看看使用原始指针时会出现的问题。
原始指针的危险
假设有一个类 Worker,它有一个成员函数 doWork()。
我们希望在另一个线程中异步执行这个函数,因此将 lambda 传递给 std::thread,并在 lambda 中捕获这个指针。
#include <iostream>
#include <thread>
class Worker {
public:
~Worker() {
std::cout << "Worker destroyed" << std::endl;
}
void doWork() {
std::cout << "Worker working..." << std::endl;
}
};
int main()
{
{
Worker w; // 局部对象
std::thread t([&w]() { // 引用捕获 w
std::this_thread::sleep_for(std::chrono::seconds(1)); // 睡1秒
w.doWork();
}
);
t.detach(); // 线程分离,lambda在后台执行
} // 离开作用域,w 被销毁
std::cout << "Leaving the scope..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2)); // 等待线程执行
return 0;
}
程序输出:
Worker destroyed
Leaving the scope...
Worker working...
我们可以看到在作用域结束后,w 被立即销毁了。
后台线程在 1 秒后才执行 w.doWork(),但 w 已不存在,访问其成员函数将导致未定义行为。
想避免这类情况发生,需要我们手动确保对象在回调执行前存活。
所幸我们可以使用 std::shared_ptr 来延迟对象的生命周期。
使用 std::shared_ptr 的优势
std::shared_ptr 通过引用计数管理对象生命周期。
当我们将一个 shared_ptr 拷贝到 lambda 内部时,引用计数增加。
只要 lambda 还持有这个拷贝,对象就不会被销毁。
这完美解决了异步任务中对象提前销毁的问题。
我们将 Worker 改为由 shared_ptr 管理,并在 lambda 中按值捕获这个 shared_ptr:
#include <iostream>
#include <thread>
#include <memory>
/* ...Worker略... */
int main()
{
{
auto work_ptr = std::make_shared<Worker>();
std::thread t([work_ptr]() {
std::this_thread::sleep_for(std::chrono::seconds(1)); // 睡1秒
std::cout << "work_ptr use_count: " << work_ptr.use_count() << std::endl;
work_ptr->doWork();
}
);
t.detach(); // 线程分离,lambda在后台执行
std::cout << "work_ptr before use_count: " << work_ptr.use_count() << std::endl;
} // 离开作用域,w 被销毁
std::cout << "Leaving the scope..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2)); // 等待线程执行
return 0;
}
程序输出:
work_ptr before use_count: 2
Leaving the scope...
work_ptr use_count: 1
Worker working...
Worker destroyed
因为 lambda 内部持有一个 shared_ptr 拷贝,所以引用计数变为 2。
(worker 一份,lambda 一份)。
离开作用域之后,因为 lambda 手里有一份shared_ptr,引用计数为1。
并且只有等到 lambda 执行完毕,其内部的 shared_ptr 销毁,引用计数归零,对象才被释放。
这样我们就确保了 doWork 调用时对象一定存活。
捕获 this 的陷阱
在类的成员函数中启动异步任务,常常需要捕获 this 以访问成员。
直接捕获原始 this 指针会面临同样的问题——对象可能在异步任务执行前被销毁。
解决方案是继承 std::enable_shared_from_this,并通过 shared_from_this() 获得一个 shared_ptr 传给 lambda。
std::enable_shared_from_this 是一个辅助模板类:
允许一个被 std::shared_ptr 管理的对象安全地生成额外的 std::shared_ptr 实例指向自己。
class Worker : public std::enable_shared_from_this<Worker> {
public:
void startAsync() {
// 获取 shared_ptr<Worker>
std::weak_ptr<Worker> weak_this = shared_from_this();
std::thread t([weak_this]() {
std::this_thread::sleep_for(std::chrono::seconds(1));
if (auto sp = weak_this.lock()) { // 提升为 shared_ptr
sp->doWork();
}
}
);
t.detach();
}
~Worker() {
std::cout << "Worker destroyed" << std::endl;
}
void doWork() {
std::cout << "Worker working..." << std::endl;
}
};
int main() {
auto w = std::make_shared<Worker>();
w->startAsync();
std::cout << "Waiting for the thread to execute." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
return 0;
}
程序输出:
Waiting for the thread to execute.
Worker working...
Worker destroyed
enable_shared_from_this 允许从 this 安全地获得一个 shared_ptr。
这里我们捕获 weak_ptr 而不是 shared_ptr,是为了避免循环引用:
如果 lambda 直接捕获 shared_from_this(),那么 lambda 内部持有了对象的 shared_ptr。
而对象又可能通过成员变量持有 lambda(例如将 lambda 存储为回调),就会形成循环。
结尾
Lambda 表达式是现代 C++ 编程的利器,它以简洁的语法提供了强大的函数式编程能力,同时保持了与底层仿函数相当的性能。
掌握其捕获规则、参数传递和适用场景,能够写出更安全、更清晰、更易维护的代码。
而lambda 与智能指针的结合,让我们能够以简洁、安全的方式管理异步任务中的资源。