一、定义
Lambda表达式是C++11引入的一种函数对象的匿名表示方法。它可以用于定义轻量级的、临时的、内联的函数对象,通常用于函数式编程的场景。
1. 语法结构
Lambda表达式的 基本语法 如下:
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type {
// 函数体 - statement
}
ambda表达式各部分说明:
[capture-list]
: 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。(parameters)
:参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。mutable
:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。->returntype
:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。{}
中是Lambda表达式的函数体,包含了实际要执行的代码逻辑。
需要注意的是:
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}
; 该lambda函数不能做任何事情
2. 关于 捕获列表
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
二、性质
- 父作用域指包含lambda函数的语句块
- 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。例如:
[=, &a, &b]
:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量[&,a, this]
:值传递方式捕捉变量a和this,引用方式捕捉其他变量
- 捕捉列表不允许变量重复传递,否则就会导致编译错误。例如:
比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
- 在块作用域以外的lambda函数捕捉列表必须为空。
- 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
- lambda表达式之间不能相互赋值,即使看起来类型相同
原因:Lambda表达式是一种匿名函数,它生成的函数对象是一个闭包。每个Lambda表达式都有其自己的类型,并且不同的Lambda表达式具有不同的类型。因此,Lambda表达式之间不能直接相互赋值。
可以使用 函数指针 / std::function
进行函数赋值
// 函数指针实现 函数赋值
void f1() {
std::cout << "hello" << std::endl;
}
void f2() {
std::cout << "hello" << std::endl;
}
int main()
{
void (*pf)();
pf = f1; // 将函数指针pf指向f1
pf(); // 调用f1函数
pf = f2; // 将函数指针pf指向f2
pf(); // 调用f2函数
return 0;
}
三、常见用法
1. 作为函数对象传递:
Lambda表达式可以作为参数传递给需要函数对象作为参数的函数,例如:
// 函数接受一个函数对象作为参数
void process(std::function<void(int)> func) {
// ...
}
// 调用时使用Lambda表达式作为参数
process([](int x) {
// Lambda表达式的逻辑
// ...
});
2. 在算法中使用:
Lambda表达式可以与标准库中的算法一起使用,用于指定算法的操作行为,例如:
std::vector<int> nums = {1, 2, 3, 4, 5};
// 使用Lambda表达式作为排序准则
std::sort(nums.begin(), nums.end(), [](int a, int b) {
return a < b;
});
// 使用Lambda表达式作为条件判断
auto it = std::find_if(nums.begin(), nums.end(), [](int num) {
return num > 3;
});
3. 自定义比较器:
Lambda表达式可以用来定义自定义的比较器,例如在容器排序或二叉搜索树中使用,例如:
struct Person {
std::string name;
int age;
};
std::vector<Person> people = {{"Alice", 25}, {"Bob", 30}, {"Charlie", 20}};
// 使用Lambda表达式定义自定义比较器
std::sort(people.begin(), people.end(), [](const Person& p1, const Person& p2) {
return p1.age < p2.age;
});
4. 异步编程:
Lambda表达式可以用于异步编程,例如与std::async一起使用,创建异步任务,例如:
auto future = std::async([]() {
// 异步任务的逻辑
// ...
return result;
});
// 获取异步任务的结果
auto result = future.get();
四、一些问题
lambda 表达式与仿函数的区别
仿函数介绍
仿函数通常通过类成员或构造函数来保存外部变量的值。仿函数本身并没有直接的语法支持来捕获外部变量。为了实现类似的功能,通常通过类的成员变量或构造函数来传递和存储外部数据。
示例
struct AddX {
int x;
AddX(int x_) : x(x_) {}
int operator()(int a) {
return a + x;
}
};
AddX add_x(10);
std::cout << add_x(5); // 输出 15
下表是lambda与仿函数的区别:
特性 | Lambda表达式 | 仿函数(Function Object) |
---|---|---|
语法 | 简洁,匿名函数 | 通过类和重载 operator() 实现 |
捕获外部变量 | 可以直接捕获外部变量 | 通过类成员或构造函数传递外部变量 |
灵活性 | 适合临时和简单的功能 | 适合复杂的功能,支持继承、状态管理等 |
内存开销 | 通常较小,但捕获外部变量时可能有额外开销 | 可能稍大,尤其是当类比较复杂时 |
可扩展性 | 灵活性较差,不能扩展为其他复杂结构(如继承) | 支持扩展,可以拥有成员函数、虚函数等 |
可读性与用途 | 简洁且易于理解,适用于局部函数对象的场景 |
lambda 表达式的底层实现原理
1. Lambda表达式的本质
Lambda 表达式本质上是一个 匿名函数对象,它是由编译器根据表达式自动生成的一个类。这个类实现了 operator()
,使得该对象可以像函数一样被调用。Lambda 表达式通常有一个捕获列表([capture]
),捕获列表决定了 Lambda 是否可以使用外部作用域中的变量。
2. Lambda 底层实现的主要步骤
2.1 生成匿名类
Lambda 表达式会被编译器转换为一个 匿名类,该类继承自一个无继承关系的基类(如果有捕获,通常会包含额外的成员变量来存储捕获的值)。这个匿名类定义了一个 operator()
方法,该方法就是 Lambda 的函数体。
- 如果 Lambda 不捕获外部变量(即空捕获列表
[]
),编译器生成的类只有operator()
,并且它不含有额外的成员。 - 如果 Lambda 捕获了外部变量(例如
[x, y]
),编译器会为每个捕获的变量生成一个成员变量,用于存储捕获的值。
示例:
auto add = [](int a, int b) { return a + b; };
编译器会将上述 Lambda 表达式转换为类似如下的匿名类:
struct AddLambda {
int operator()(int a, int b) const {
return a + b;
}
};
2.2 捕获外部变量
如果 Lambda 捕获了外部变量,编译器会为每个捕获的变量生成一个成员变量。捕获列表决定了 Lambda 如何访问这些变量(按值捕获或按引用捕获)。
- 按值捕获(
[x]
):编译器会在匿名类中生成一个x
的副本作为成员变量。 - 按引用捕获(
[&x]
):编译器会将x
的引用存储为成员变量。
示例:
int x = 10;
auto add = [x](int a) { return a + x; };
这将被转换成如下的匿名类:
struct AddLambda {
int x; // 存储捕获的外部变量 x
AddLambda(int x_) : x(x_) {} // 构造函数,初始化 x
int operator()(int a) const {
return a + x;
}
};
2.3 编译器生成对象
Lambda 会被编译成一个类型为该匿名类的对象。在编译时,Lambda 会根据其捕获方式被转化为该匿名类的一个实例,并且通过该实例来执行相应的操作。
AddLambda add_lambda(x); // 实例化匿名类对象
std::cout << add_lambda(5); // 调用 operator()
3. Lambda 表达式的不同捕获方式
Lambda 的捕获方式直接影响其底层实现,捕获可以分为按值捕获和按引用捕获两种情况,编译器为每种捕获方式生成不同的代码。
3.1 按值捕获(Capture by value)
按值捕获时,编译器会将捕获的变量的副本保存在 Lambda 对象的成员中。这意味着 Lambda 对象中会包含捕获的变量的一个 拷贝。
示例:
int x = 10;
auto add = [x](int a) { return a + x; };
这将转换为类似如下的类:
struct AddLambda {
int x; // 捕获的变量 x
AddLambda(int x_) : x(x_) {} // 构造函数初始化 x
int operator()(int a) const {
return a + x;
}
};
3.2 按引用捕获(Capture by reference)
按引用捕获时,编译器会将捕获的变量的引用保存到 Lambda 对象中,而不是拷贝值。这意味着 Lambda 可以修改外部变量的值。
示例:
int x = 10;
auto add = [&x](int a) { return a + x; };
这将转换为类似如下的类:
struct AddLambda {
int& x; // 捕获的变量 x 的引用
AddLambda(int& x_) : x(x_) {} // 构造函数初始化引用 x
int operator()(int a) const {
return a + x;
}
};
3.3 混合捕获(Capture by value and reference)
你也可以同时按值和按引用捕获外部变量。在这种情况下,编译器会为每个捕获的变量生成相应的成员变量,按值捕获生成副本,按引用捕获生成引用。
示例:
int x = 10, y = 20;
auto add = [x, &y](int a) { return a + x + y; };
这将转换为类似如下的类:
struct AddLambda {
int x; // 捕获的变量 x(按值)
int& y; // 捕获的变量 y(按引用)
AddLambda(int x_, int& y_) : x(x_), y(y_) {} // 构造函数初始化
int operator()(int a) const {
return a + x + y;
}
};
4. Lambda 表达式的返回类型
Lambda 表达式的返回类型通常由编译器推导,但也可以显式指定返回类型。返回类型的推导是通过 ->
后面的类型或者编译器的自动推导来完成的。
示例:
auto add = [](int a, int b) -> int { return a + b; };
在底层实现中,编译器会根据表达式的返回值推导出返回类型,并将其用在生成的匿名类的 operator()
的返回类型中。
5. Lambda 表达式的性能优化
编译器通常会对 Lambda 表达式进行优化,包括:
- 内联优化:如果 Lambda 的大小适中,编译器可能会将其内联,消除函数调用开销。
- 捕获优化:对于按值捕获的 Lambda,编译器可以在适当的情况下共享捕获的值,减少不必要的内存开销。
6. 使用类型擦除的情况(std::function
)
当 Lambda 表达式被传递给需要 std::function
或其他通用函数指针类型的 API 时,编译器会使用类型擦除技术将 Lambda 的具体类型转化为一个可以存储不同类型的 std::function
。这种情况下,Lambda 会被包装成一个类型擦除的对象,这样它可以作为一个通用的函数对象传递。
7. 总结
Lambda 表达式的底层实现可以总结为以下几个关键点:
- 匿名类:Lambda 表达式会被编译器转换为一个匿名类,该类实现了
operator()
。 - 捕获外部变量:捕获的外部变量会被存储为类的成员变量,按值捕获或按引用捕获会影响 Lambda 对象的内存布局。
- 返回类型和推导:Lambda 表达式的返回类型可以由编译器自动推导,也可以显式指定。
- 类型擦除:在需要时,Lambda 表达式可以被包装为通用的函数对象(如
std::function
),以便类型擦除。