Lambda表达式
1. 什么是Lambda表达式?
通俗理解 :Lambda表达式是一种匿名函数(未命名的函数对象),你可以在需要函数的地方就地定义它,而无需单独写一个命名函数或函数对象。它让代码更简洁、更灵活,尤其适用于短小的、一次性使用的函数逻辑,比如作为参数传递给算法(如std::sort
, std::for_each
)。
核心思想:就地定义,即时使用。
2. Lambda表达式的基本语法
一个完整的Lambda表达式的语法如下:
cpp
[capture-list] (parameters) mutable -> return-type {
// 函数体
}
其中,只有 [capture-list]
和 { 函数体 }
是必须的,其他部分在特定情况下可以省略。一个最简单的Lambda如下:
cpp
[]{ std::cout << "Hello, Lambda!" << std::endl; }();
// 输出: Hello, Lambda!
// 注意最后的 `()` 是直接调用这个Lambda
让我们来分解它的各个部分。
3. 捕获列表 (Capture List) [...]
这是Lambda最独特和强大的特性之一。它定义了Lambda函数体内部如何访问其外部作用域中的变量。
a) 值捕获 [var]
将外部变量的值复制一份到Lambda内部。Lambda内部修改这个副本不会影响外部的原变量。
cpp
int main() {
int x = 10;
auto lambda_val = [x] { // 捕获 x 的值
std::cout << "Inside lambda (by value): " << x << std::endl;
// x++; // 错误!默认情况下,值捕获的变量是 const 的,不可修改
};
x = 20; // 修改外部 x
lambda_val(); // 调用Lambda
std::cout << "Outside lambda: " << x << std::endl;
return 0;
}
输出:
Inside lambda (by value): 10
Outside lambda: 20
解释 :Lambda在创建的那一刻就复制了x
的值(10)。之后外部x
变为20,但Lambda内部使用的仍然是它创建时捕获的副本10。
b) 引用捕获 [&var]
捕获外部变量的引用。在Lambda内部操作这个变量就是在操作外部的原变量。
cpp
int main() {
int x = 10;
auto lambda_ref = [&x] { // 捕获 x 的引用
std::cout << "Inside lambda (by ref): " << x << std::endl;
x++; // 可以修改,因为它是引用
};
lambda_ref(); // 调用Lambda
std::cout << "Outside lambda: " << x << std::endl; // x 被修改了
return 0;
}
输出:
Inside lambda (by ref): 10
Outside lambda: 11
c) 隐式捕获
让编译器根据我们在函数体中的使用,自动推断需要捕获哪些变量。
[=]
:以值捕获的方式捕获所有外部变量。[&]
:以引用捕获的方式捕获所有外部变量。
cpp
int main() {
int a = 1, b = 2;
auto lambda_implicit = [=] { // 以值方式捕获所有需要的变量(a 和 b)
std::cout << "a + b = " << a + b << std::endl;
};
auto lambda_implicit_ref = [&] { // 以引用方式捕获所有需要的变量
a++;
b++;
};
lambda_implicit();
lambda_implicit_ref();
std::cout << "a: " << a << ", b: " << b << std::endl;
return 0;
}
输出:
a + b = 3
a: 2, b: 3
最佳实践 :尽量避免使用隐式捕获 [=]
和 [&]
,显式地列出需要捕获的变量可以使代码更清晰、更安全。
d) 混合捕获
你可以混合使用值和引用捕获,以及隐式捕获。
[=, &var]
:默认以值方式捕获所有变量,但变量var
以引用方式捕获。[&, var]
:默认以引用方式捕获所有变量,但变量var
以值方式捕获。
cpp
int main() {
int a = 1, b = 2, c = 3;
auto lambda_mixed = [=, &c] { // a, b 以值捕获,c 以引用捕获
std::cout << "a: " << a << ", b: " << b << std::endl;
c++; // 修改外部的 c
// a++; // 错误:值捕获的变量是 const 的
};
lambda_mixed();
std::cout << "c: " << c << std::endl;
return 0;
}
输出:
a: 1, b: 2
c: 4
e) 捕获 this
指针 [this]
或 [=]
在类的成员函数中,Lambda可以通过捕获this
指针来访问类的成员变量和函数。
cpp
class MyClass {
public:
int value = 42;
void print() {
// 捕获 this,从而可以访问成员 value
auto lambda = [this] {
std::cout << "Member value: " << value << std::endl; // 实际上是 this->value
};
lambda();
}
};
int main() {
MyClass obj;
obj.print();
return 0;
}
输出:
Member value: 42
注意 :在C++11中,[=]
也会隐式捕获this
指针。从C++20开始,[=, this]
被标记为重复捕获而不再合法,建议直接使用 [this]
或 [=]
。
4. 参数列表 (parameters)
和普通函数的参数列表几乎一样。可以为空()
,也可以定义参数。
cpp
#include <iostream>
#include <string>
int main() {
// 带参数的Lambda
auto greet = [](const std::string& name) {
std::cout << "Hello, " << name << "!" << std::endl;
};
greet("Alice"); // 调用
// 带多个参数,用于算法
auto add = [](int a, int b) -> int {
return a + b;
};
std::cout << "5 + 3 = " << add(5, 3) << std::endl;
return 0;
}
5. 可变规范 (mutable) mutable
默认情况下,对于值捕获 的变量,Lambda将其视为const
值,你无法在函数体内修改它们。如果你需要修改这些副本(注意:修改的是副本,不影响外部变量),就需要使用 mutable
关键字。
cpp
int main() {
int x = 10;
auto lambda = [x]() mutable { // 使用 mutable
x++; // 现在可以修改值捕获的副本了
std::cout << "Inside mutable lambda: " << x << std::endl;
};
lambda();
lambda(); // 每次调用,其内部捕获的副本都会保留修改后的状态
std::cout << "Outside lambda: " << x << std::endl; // 外部的 x 从未被改变
return 0;
}
输出:
Inside mutable lambda: 11
Inside mutable lambda: 12
Outside lambda: 10
重要 :mutable
允许你修改的是Lambda自身捕获的副本 ,而不是外部变量。并且它使得Lambda调用运算符 ()
不再是 const
的。
这个东西说实在的,几乎没用。
6. 返回类型 -> return-type
大多数情况下,编译器可以推断出Lambda的返回类型。
- 如果函数体只包含一个
return
语句,返回类型就是该表达式的类型。 - 如果函数体包含多个返回语句,且类型不同,或者形式复杂,编译器无法推断,你就必须显式指定返回类型。
cpp
int main() {
// 编译器推断返回类型为 int
auto inferred = [](int a, int b) { return a + b; };
// 必须显式指定返回类型为 double
// 否则多个return语句类型不同(int 和 double),编译器会报错或选择int
auto explicit_return = [](int a, int b) -> double {
if (a == 0) {
return 0; // int
} else {
return static_cast<double>(b) / a; // double
}
};
std::cout << inferred(5, 3) << std::endl; // 8
std::cout << explicit_return(5, 3) << std::endl; // 0.6
return 0;
}
7. 函数体 { ... }
和普通函数的函数体没有任何区别,可以包含任何有效的C++语句。
8. Lambda的实际应用场景
a) 与STL算法结合(最常用)
这是Lambda的"主场",它让STL算法的调用变得异常简洁。
cpp
#include <iostream>
#include <vector>
#include <algorithm> // for std::for_each, std::sort, std::find_if
int main() {
std::vector<int> numbers = {4, 2, 5, 1, 3};
// 1. 使用 Lambda 进行排序(自定义比较器)
std::sort(numbers.begin(), numbers.end(),
[](int a, int b) { return a > b; }); // 降序排列
// numbers 现在是 {5, 4, 3, 2, 1}
// 2. 使用 Lambda 遍历元素 (替代传统的函数对象)
std::for_each(numbers.begin(), numbers.end(),
[](int n) { std::cout << n << " "; });
std::cout << std::endl;
// 3. 使用 Lambda 查找条件元素
int threshold = 3;
auto it = std::find_if(numbers.begin(), numbers.end(),
[threshold](int n) { return n < threshold; });
if (it != numbers.end()) {
std::cout << "First number less than " << threshold << " is " << *it << std::endl;
}
return 0;
}
b) 作为回调函数
异步编程、事件驱动编程中,Lambda非常适合作为一次性回调。
cpp
#include <iostream>
#include <thread>
#include <chrono>
// 模拟一个异步操作,完成后调用回调函数
void asyncOperation(std::function<void(int)> callback) {
std::cout << "Operation started..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
int result = 42;
std::cout << "Operation finished. Calling callback." << std::endl;
callback(result); // 调用传入的Lambda
}
int main() {
// 就地定义一个Lambda作为回调传给异步函数
asyncOperation([](int result) {
std::cout << "Callback received result: " << result << std::endl;
});
// 主线程等待一下,确保异步操作完成(实际中会用更复杂的方式同步)
std::this_thread::sleep_for(std::chrono::seconds(3));
return 0;
}
c) 封装局部状态(闭包)
Lambda可以捕获并持有局部状态,形成一个闭包(Closure)。这在C++11之前需要手动编写一个仿函数类才能实现。
cpp
#include <iostream>
// 创建一个计数器生成器函数
std::function<int()> makeCounter(int start = 0) {
// 返回一个Lambda,它捕获了局部变量 `count` 的状态
return [=]() mutable { // 值捕获 start,mutable 允许修改副本
return start++;
};
}
int main() {
auto counter1 = makeCounter(10);
auto counter2 = makeCounter();
std::cout << counter1() << std::endl; // 10
std::cout << counter1() << std::endl; // 11
std::cout << counter2() << std::endl; // 0
std::cout << counter1() << std::endl; // 12
std::cout << counter2() << std::endl; // 1
return 0;
}
解释 :每次调用makeCounter
都会创建一个新的、独立的Lambda对象,每个对象都持有自己捕获的start
变量的副本。调用这个Lambda就会修改并返回它自己内部的那个副本。这就是闭包的行为。
9. Lambda的本质
重要知识点 :Lambda表达式并不是黑魔法。编译器在背后会为你自动生成一个独一无二的匿名仿函数类(Functor Class)。
你的Lambda:
cpp
int x = 10;
auto lambda = [x](int y) mutable -> int { return x + y; };
会被编译器转换成类似这样的代码:
cpp
// 编译器生成的匿名类
class __Some_Unique_Compiler_Generated_Name__ {
private:
int x; // 值捕获的变量变成了成员变量
public:
__Some_Unique_Compiler_Generated_Name__(int captured_x) : x(captured_x) {} // 构造函数
// 重载函数调用运算符 operator()
int operator()(int y) const { // 注意:如果没有 mutable,这里是 const
return x + y;
}
// 如果有 mutable, operator() 就不是 const 的
};
// 你的 `auto lambda = ...` 这一行实际上相当于:
__Some_Unique_Compiler_Generated_Name__ lambda(x);
理解这一点非常重要,它解释了:
- 为什么Lambda可以有状态:因为它的状态保存在其类的成员变量中。
- 为什么值捕获的变量默认是
const
的 :因为生成的operator()
默认是const
成员函数。 - 为什么需要
mutable
:mutable
去掉了生成的operator()
的const
限定符。 - Lambda的大小:Lambda的大小取决于它捕获的变量的大小。
- Lambda如何传递:和传递类对象一样(值传递、引用传递等)。
总结
特性 | 语法 | 说明 |
---|---|---|
捕获列表 | [x, &y] |
定义如何访问外部变量 |
参数列表 | (int a, double b) |
和普通函数一样 |
可变规范 | mutable |
允许修改值捕获的变量副本 |
返回类型 | -> double |
可省略(编译器推断),复杂时需显式指定 |
函数体 | { ... } |
包含要执行的代码 |