Lambda表达式是C++11引入的一项重要特性,它极大地改变了我们编写匿名函数的方式。
一、为什么会有Lambda表达式
在C++11之前,当我们需要传递一个简单的函数时,通常有以下几种选择:
1.1、定义一个单独的函数
// 单独定义的比较函数
bool compareInts(int a, int b) {
return a < b;
}
void functionExample() {
std::vector<int> numbers = {4, 2, 5, 1, 3};
// 使用函数指针传递比较逻辑
std::sort(numbers.begin(), numbers.end(), compareInts);
for (int num : numbers) {
std::cout << num << " ";
}
}
- 对于只使用一次的逻辑来说过于繁琐
- 无法捕获上下文变量
1.2、使用函数对象(重载operator()的类)
// 函数对象类
class GreaterThan {
int threshold;
public:
GreaterThan(int t) : threshold(t) {}
bool operator()(int value) const {
return value > threshold;
}
};
void functorExample() {
std::vector<int> numbers = {1, 3, 5, 7, 9, 2, 4, 6, 8};
// 使用函数对象查找第一个大于5的数
auto it = std::find_if(numbers.begin(), numbers.end(), GreaterThan(5));
if (it != numbers.end()) {
std::cout << "First number greater than 5: " << *it << std::endl;
// 输出: First number greater than 5: 7
}
}
- 对于简单逻辑显得过于复杂
- 需要额外定义一个类
1.3、使用函数指针
函数指针是指向函数的指针变量,它存储的是函数的入口地址。函数指针的类型由函数的返回类型和参数类型共同决定:
// 函数声明
ReturnType FunctionName(ParameterType1, ParameterType2, ...);
// 对应的函数指针类型
ReturnType (*PointerName)(ParameterType1, ParameterType2, ...);
举个例子:
// 普通函数
double square(double x) {
return x * x;
}
// 使用函数指针作为参数的函数
void transformVector(std::vector<double>& vec, double (*func)(double)) {
for (auto& elem : vec) {
elem = func(elem);
}
}
void functionPointerExample() {
std::vector<double> numbers = {1.0, 2.0, 3.0, 4.0};
// 传递square函数指针
transformVector(numbers, square);
for (double num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;
}
- 无法捕获上下文
- 语法较为晦涩
- 不能内联优化,可能有性能损失
- 只能指向静态函数,不能指向成员函数
方式 | 优点 | 缺点 |
---|---|---|
单独函数 | 简单直接,可重用 | 污染命名空间,无法捕获上下文 |
函数对象 | 可保持状态,灵活 | 需要定义额外类,代码分散 |
函数指针 | 兼容C,可动态选择函数 | 语法复杂,无法内联,不能捕获上下文 |
Lambda表达式解决了这些问题,提供了一种简洁、内联的方式来定义匿名函数。
二、Lambda表达式基本语法
[capture](parameters) -> return_type {
// 函数体
}
捕获列表(capture):指定lambda表达式如何访问外部变量
参数列表(parameters):与普通函数的参数列表类似
返回类型(return_type):可选的,编译器通常可以推导
函数体:包含lambda执行的代码
举个例子:
auto greet = []() {
std::cout << "Hello, World!" << std::endl;
};
greet(); // 输出: Hello, World!
这个lambda没有捕获任何变量,没有参数,也不返回任何值(返回void)。
2.1、捕获列表
捕获列表决定了lambda表达式如何访问外部作用域中的变量。捕获方式有多种:
2.1.1、值捕获
int x = 10;
auto lambda = [x]() {
std::cout << x << std::endl;
};
x = 20;
lambda(); // 输出: 10
值捕获创建了变量的副本,lambda内部使用的是捕获时的值。
2.1.2、引用捕获
int x = 10;
auto lambda = [&x]() {
std::cout << x << std::endl;
};
x = 20;
lambda(); // 输出: 20
引用捕获使用变量的引用,lambda内部访问的是变量的当前值。
2.1.3、隐式捕获
int x = 10;
int y = 20;
auto lambda1 = [=]() { std::cout << x << ", " << y << std::endl; }; // 值捕获所有
auto lambda2 = [&]() { std::cout << x << ", " << y << std::endl; }; // 引用捕获所有
[=]
表示值捕获所有外部变量,[&]
表示引用捕获所有外部变量。
2.1.5、this指针捕获
class MyClass {
int value = 42;
public:
void print() {
auto lambda = [this]() {
std::cout << value << std::endl;
};
lambda();
}
};
2.1.4、混合捕获
int x = 10;
int y = 20;
auto lambda = [=, &y]() {
// 值捕获x,引用捕获y
std::cout << x << ", " << y << std::endl;
};
在类的成员函数中,lambda可以捕获this
指针来访问类的成员:
2.2、参数列表
Lambda表达式的参数列表与普通函数类似:
auto add = [](int a, int b) {
return a + b;
};
std::cout << add(5, 3) << std::endl; // 输出: 8
2.3、返回类型
返回类型通常可以省略,编译器会自动推导:
auto square = [](int x) { return x * x; }; // 返回类型推导为int
当函数体包含多个return语句且类型不同,或者返回类型不明显时,需要显式指定:
auto conditional = [](int x) -> double {
if (x > 0) return 1.0;
else return 0.0;
};
2.4、可变Lambda (mutable)
默认情况下,值捕获的变量在lambda内是const的。使用mutable
关键字可以修改这些副本:
int x = 0;
auto counter = [x]() mutable {
x++; // 没有mutable会编译错误
std::cout << x << std::endl;
};
counter(); // 输出: 1
counter(); // 输出: 2
std::cout << x << std::endl; // 输出: 0 (原始x未被修改)
2.5、Lambda表达式的类型
每个lambda表达式都有一个唯一的、未命名的类型。要存储lambda,通常使用auto
或std::function
:
auto lambda = []() { /* ... */ };
std::function<void()> lambda = []() { /* ... */ };
三、用法
3.1、作为函数参数
Lambda常用于STL算法中作为谓词:
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::for_each(numbers.begin(), numbers.end(), [](int n) {
std::cout << n << " ";
});
3.2、立即调用
auto make_multiplier = [](int factor) {
return [factor](int x) { return x * factor; };
};
auto times3 = make_multiplier(3);
std::cout << times3(5) << std::endl; // 输出: 15
3.3、泛型Lambda (C++14)
C++14引入了泛型lambda,允许使用auto
参数:
auto print = [](const auto& value) {
std::cout << value << std::endl;
};
print(42); // int
print(3.14); // double
print("Hello"); // const char*
3.4、捕获表达式 (C++14)
C++14允许在捕获列表中初始化变量:
int x = 10;
auto lambda = [y = x + 5]() {
std::cout << y << std::endl; // 输出: 15
};
3.5、constexpr Lambda (C++17)
C++17允许lambda在编译时求值:
constexpr auto square = [](int x) { return x * x; };
static_assert(square(5) == 25);
3.6、举个例子
std::vector<std::pair<int, std::string>> items = {
{3, "three"}, {1, "one"}, {4, "four"}, {2, "two"}
};
// 按第一个元素升序排序
std::sort(items.begin(), items.end(),
[](const auto& a, const auto& b) { return a.first < b.first; });
// 按第二个元素长度降序排序
std::sort(items.begin(), items.end(),
[](const auto& a, const auto& b) { return a.second.size() > b.second.size(); });
四、Lambda存在的限制
- 不能有默认参数:lambda表达式不支持默认参数
- 不能递归调用自身:除非通过
std::function
或捕获自身引用 - 不能是虚函数:lambda表达式不能是虚函数
五、考点
5.1、为什么直接使用auto存储lambda通常比std::function更高效
std::function
使用了类型擦除(Type Erasure)技术- 它可以存储任何可调用对象(函数指针、成员函数指针、函数对象、Lambda等)
- 这种通用性带来了运行时开销
5.2、Lambda表达式在编译器内部是如何实现的?
编译器会为每个Lambda生成一个唯一的匿名类,其中:
- 捕获的变量成为该类的成员变量
- operator()被重载为Lambda的函数体
- 根据捕获方式决定成员变量是值还是引用
int x = 10;
auto lambda = [x](int y) { return x + y; };
// ====> 编译后
class __AnonymousLambda {
int x;
public:
__AnonymousLambda(int x) : x(x) {}
int operator()(int y) const { return x + y; }
};
5.3、什么是悬空引用
std::function<void()> createLambda() {
int x = 10;
return [&x]() { std::cout << x; }; // x已销毁!
}
返回的lambda表达式无法再次获得x。
5.4、怎么递归Lambda表达式
由于Lambda没有名称,无法直接递归调用自己。可以通过以下方式实现:
std::function<int(int)> factorial = [&factorial](int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
};
5.5、解释以下捕获方式的区别:[]
, [=]
, [&]
, [this]
[]
:不捕获任何外部变量[=]
:值捕获所有外部变量(创建副本)[&]
:引用捕获所有外部变量(使用引用)[this]
:捕获当前类的this指针,可以访问成员变量和函数
5.6、 Lambda表达式与普通函数有什么区别?
- 匿名性:Lambda是匿名函数,无需命名
- 捕获能力:可以捕获上下文中的变量
- 内联定义:可以在使用的地方直接定义
- 类型:每个Lambda有唯一的、编译器生成的类型
- 灵活性:可以更方便地作为参数传递