1 Lambda 表达式简介
1.1 Lambda 表达式的定义与概念
Lambda 表达式是 C++11 引入的一种函数对象的匿名表示方法,它的定义与概念基于数学中的 λ 演算。Lambda 表达式为程序员提供了一种更加简洁、灵活的方式来定义轻量级的、临时的、内联的函数对象,通常用于函数式编程的场景。
Lambda表达式的基本语法结构如下:
[capture-list](parameters) mutable -> return-type { body }
其中:
- [capture-list]:捕获列表,用于捕获上下文中的变量供 Lambda 函数使用。它总是出现在 Lambda 函数的开始位置,编译器根据[]来判断接下来的代码是否为 Lambda 函数。捕获列表可以包含按值捕获或按引用捕获的变量。
- (parameters):参数列表,与普通函数的参数列表类似,用于定义 Lambda 函数的输入参数。如果没有参数,参数列表可以省略。
- mutable:可选的,用于指示 Lambda 函数体内部可以修改捕获的按值捕获的变量。
- -> return-type:返回类型,用于指定Lambda函数的返回类型。如果 Lambda 函数体中没有返回语句,或者返回类型可以由编译器推断出来,则可以省略返回类型。
- { body }:函数体,即 Lambda 函数的实现部分,包含 Lambda 函数的具体逻辑。
Lambda表达式的主要特点包括:
- 匿名性:Lambda 表达式没有具体的函数名,它们以匿名的方式定义,并且可以在需要函数对象的地方直接使用。
- 简洁性:Lambda 表达式允许就地定义函数对象,无需事先声明或定义独立的函数或函数对象,从而简化了代码结构。
- 闭包特性:Lambda 表达式能够捕获所在作用域中的变量,并将这些变量与函数体一起封装成一个闭包(closure)。这使得 Lambda 表达式能够访问和操作外部变量,增加了代码的灵活性和可重用性。
- Lambda 表达式在 C++11 中起到了重要的作用,它填补了 C++ 在函数式编程方面的空缺,使得 C++ 程序员能够更加方便地实现回调函数、代理等功能,提高代码的可读性和可维护性。同时,Lambda 表达式也与其他 C++11 特性(如 STL 算法、自动类型推导等)紧密结合,为 C++ 程序员提供了更加强大和灵活的工具集。
1.2 Lambda 表达式与函数对象、函数指针的比较
Lambda 表达式与函数对象、函数指针在功能上有一定的相似性,但在使用方式、语法和灵活性等方面存在显著的区别。下面详细比较这三者:
(1)Lambda 表达式
Lambda 表达式是 C++11 引入的一种匿名函数对象,它允许在代码中直接定义小型函数,而无需显式地声明一个函数或函数对象。Lambda 表达式可以捕获其所在作用域中的变量,从而可以访问和操作这些变量。Lambda 表达式通常用于需要临时函数或回调函数的场景。
优点:
- 语法简洁,可以直接在需要的地方定义。
- 易于捕获上下文中的变量。
- 支持闭包特性,可以保留状态。
缺点:
- 由于是匿名的,可能在一些需要明确函数名或类型的场合不适用。
(2)函数对象
函数对象(也称为仿函数)是重载了函数调用运算符 operator() 的类的对象。它们可以像普通函数一样被调用,并且通常用于作为算法或容器的参数。函数对象可以包含状态,并且可以通过构造函数或成员函数来修改其行为。
优点:
- 可以像普通对象一样使用,有状态和方法。
- 可以通过继承和多态扩展功能。
- 命名明确,易于理解和维护。
缺点:
- 需要显式地定义类,相对较为繁琐。
- 不如 Lambda 表达式简洁。
(3)函数指针
函数指针是指向函数的指针变量,它们可以用来动态地调用函数。函数指针在 C++ 中一直存在,是 C 语言风格的函数回调的主要方式。
优点:
- 简洁,语法直观。
- 可以用于动态地调用不同的函数。
缺点:
- 无法直接捕获上下文中的变量(除非使用全局变量或静态变量)。
- 对于复杂的函数逻辑,使用函数指针可能不够灵活。
(4)比较
- 语法与简洁性:Lambda 表达式在语法上最为简洁,可以直接在代码中定义。函数对象需要定义类,而函数指针则需要声明指针变量。
- 状态与捕获:Lambda 表达式和函数对象都可以包含状态,并且可以捕获上下文中的变量。而函数指针本身不包含状态,也无法直接捕获变量。
- 灵活性:Lambda 表达式在定义和使用上最为灵活,可以根据需要捕获不同的变量,并且支持闭包特性。函数对象虽然不如 Lambda 表达式简洁,但也可以通过继承和多态实现更复杂的功能。函数指针在灵活性上相对较弱,主要用于简单的函数回调。
总的来说,Lambda 表达式、函数对象和函数指针在 C++ 中各自有其适用的场景。Lambda 表达式适用于需要临时函数或回调函数的场合,函数对象适用于需要更复杂状态和行为控制的场合,而函数指针则更适用于简单的函数回调。在选择使用哪种方式时,应根据具体需求和场景进行权衡。
2 Lambda 表达式的捕获子句
2.1 捕获子句的概念与语法
Lambda 表达式的捕获子句(Capture Clause)是一个非常重要的概念,它决定了 Lambda 表达式能够访问哪些外部变量。Lambda 表达式的捕获子句允许 Lambda 函数体内部使用在其定义范围之外的局部变量,即捕获这些变量的值或引用。
捕获子句的概念
捕获子句的主要作用是定义 Lambda 表达式可以访问哪些在其外部作用域中定义的变量。Lambda 表达式通过捕获子句来“捕获”这些外部变量,从而在 Lambda 函数体内部使用它们。捕获可以是按值捕获(value capture)或按引用捕获(reference capture),这决定了 Lambda 表达式内部如何访问这些外部变量。
- 按值捕获:Lambda 表达式会创建所捕获变量的副本,并在 Lambda 函数体内部使用这个副本。这意味着如果原始变量在Lambda表达式被调用之后被修改,Lambda函数体内部使用的变量值不会改变。
- 按引用捕获:Lambda 表达式不会创建所捕获变量的副本,而是直接在 Lambda 函数体内部使用这些变量的引用。因此,如果原始变量在 Lambda 表达式被调用之后被修改,这些修改将反映在 Lambda 函数体内部。
捕获子句的语法
捕获子句的语法形式如下:
[capture-list] (parameter-list) -> return-type { lambda-body }
其中,capture-list就是捕获子句,它定义了Lambda表达式可以访问的外部变量。捕获列表可以包含以下类型的捕获:
- 按值捕获:使用变量的名字(不带 &)。例如:[x] 表示按值捕获变量 x。
- 按引用捕获:使用变量的名字前加 &。例如:[&x] 表示按引用捕获变量 x。
- 隐式捕获:使用 = 或 & 作为捕获列表的开始,表示默认按值或按引用捕获所有外部变量。例如:[=] 表示默认按值捕获所有外部变量,[&] 表示默认按引用捕获所有外部变量。
混合捕获:捕获列表中可以同时包含按值和按引用捕获的变量。例如:[x, &y] 表示按值捕获x并按引用捕获y。
2.2 值捕获与引用捕获
值捕获和引用捕获是两种不同的捕获策略,它们决定了 Lambda 表达式如何访问和存储这些外部变量。
值捕获(Value Capture)
当使用值捕获时,Lambda 表达式会创建所捕获变量的副本,并在 Lambda 函数体内部使用这个副本。这意味着 Lambda 函数体内部使用的是捕获时变量的值,而不是变量后续的修改值。这种捕获方式确保了 Lambda 函数体内部的变量值不会受到外部变量变化的影响。
值捕获的语法是在捕获列表中直接列出变量的名称,而不加任何修饰符。例如:
int x = 10;
auto lambda = [x]() { std::cout << x << std::endl; };
x = 20; // 修改x的值
lambda(); // 输出:10
在上面的例子中,变量 x 通过值捕获被 Lambda 表达式捕获。尽管在 Lambda 表达式定义之后修改了 x 的值,但 Lambda 函数体内部仍然使用的是捕获时的值(即 10)。
注意:按值捕获的变量不允许在 Lambda 函数体内部做修改(但是可以通过使用 mutable 限定符做修改,后面会讲解),如下的代码会编译失败:
int x = 10;
auto lambda = [x]() {
x+=10; // 编译失败
};
引用捕获(Reference Capture)
当使用引用捕获时,Lambda 表达式不会创建所捕获变量的副本,而是直接在 Lambda 函数体内部使用这些变量的引用。这意味着 Lambda 函数体内部使用的是外部变量的实际引用,任何对外部变量的修改都会反映在 Lambda 函数体内部。
引用捕获的语法是在捕获列表中列出变量的名称前加上 & 修饰符。例如:
int x = 10;
auto lambda = [&x]() { std::cout << x << std::endl; };
x = 20; // 修改x的值
lambda(); // 输出:20
在这个例子中,变量 x 通过引用捕获被 Lambda 表达式捕获。因此,当在 Lambda 表达式定义之后修改了 x 的值时,Lambda 函数体内部也会反映出这个变化,输出的是修改后的值(即 20)。
混合捕获
Lambda 表达式还支持混合捕获,即同时按值和按引用捕获不同的变量。这可以通过在捕获列表中同时列出按值捕获和按引用捕获的变量来实现。例如:
int a = 10;
int& b = ...; // 假设b是某个整数的引用
auto lambda = [a, &b]() { std::cout << a << " " << b << std::endl; };
在这个例子中,变量 a 通过值捕获被捕获,而变量 b 通过引用捕获被捕获。这样,Lambda 函数体内部将使用a的副本和b的引用。
注意事项
- 引用捕获需要谨慎使用,因为它可能导致悬挂引用(dangling reference)的问题。如果引用的外部变量在 Lambda 表达式被调用之前被销毁或重新赋值,那么 Lambda 函数体内部将访问一个无效的内存地址或未定义的值。
- 值捕获相对更安全,因为它不依赖于外部变量的生命周期。但需要注意的是,如果捕获的变量很大,值捕获可能会导致不必要的内存开销。
- 在某些情况下,可能需要根据具体情况选择值捕获或引用捕获。例如,如果需要在 Lambda 表达式中修改外部变量的值,则应该使用引用捕获。如果只需要读取外部变量的值,并且不希望受到外部变量变化的影响,则应该使用值捕获。
2.3 隐式捕获
上一章节 “2.2 值捕获与引用捕获” 属于显式捕获,即在 Lambda 的捕获列表中明确指定要捕获的变量。与此对应的是隐式捕获,该类型的捕获则是通过捕获列表中的两个特殊符号[&]和[=]来实现的,它们分别表示按引用捕获和按值捕获所有外部变量。
- [&]:隐式按引用捕获所有外部变量。
- [=]:隐式按值捕获所有外部变量。
隐式捕获的好处是简洁,不需要在捕获列表中列出所有要捕获的变量。但是,它也可能导致一些意料之外的行为,特别是当你不小心捕获了不需要的变量,或者捕获了不应该以值或引用方式捕获的变量时。
下面是一个使用隐式捕获的 Lambda 表达式的例子:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
int a = 10;
int b = 20;
std::vector<int> v = { 1, 2, 3, 4, 5 };
// 使用隐式按值捕获
auto lambda1 = [=]() { std::cout << "a = " << a << "; b = " << b << std::endl; };
// 使用隐式按引用捕获
auto lambda2 = [&]() { std::cout << "a = " << a << "; b = " << b << std::endl; };
a = 100;
b = 200;
lambda1();
lambda2();
return 0;
}
上面代码的输出为:
a = 10; b = 20
a = 100; b = 200
在这个例子中,第一个 Lambda 表达式使用 [=] 隐式按值捕获了变量 a、b。因此,在 Lambda 体内,a 的值是 10,b 的值是 20,不会受到外部 a、b 值变化的影响。第二个 Lambda 表达式使用 [&] 隐式按引用捕获了变量 a、b。因此,在 Lambda 体内,a、b 的值与外部 a、b 的值是绑定的,如果外部 a、b 的值改变,Lambda 体内的 a、b 的值也会相应改变。
Lambda 表达式也支持混用显式捕获和隐式捕获,这种方式提供了更细粒度的控制,使得在 Lambda 内部能够灵活地管理外部变量的访问方式。
注意:混合使用时,要求捕获列表中第一个元素必须是隐式捕获( & 或 = ),然后后面可以跟上显式指定的变量名和符号。
#include <iostream>
using namespace std;
int main()
{
int a = 1;
int b = 2;
auto lambda = [=, &b] {
printf("a = %d\n", a);
printf("b = %d\n", b);
};
a = 3;
b = 4;
lambda();
return 0;
}
上面代码的输出为:
a = 1
b = 4
3 Lambda 表达式的参数列表
3.1 参数列表的语法
Lambda 表达式的参数列表的语法与普通函数的参数列表类似,用于定义 Lambda 函数所接受的输入参数。参数列表是 Lambda 表达式语法结构中的一个重要部分,它出现在捕获列表之后,用于指定 Lambda 函数接收的参数类型和数量。
Lambda参数列表的语法形式如下:
(parameter1, parameter2, ..., parameterN)
其中,parameter1, parameter2, …, parameterN 是 Lambda 函数的参数,每个参数由参数类型和参数名组成。参数类型可以是任何有效的 C++ 类型,包括基本类型、类类型、指针类型等。参数名则是用于在 Lambda 函数体内部引用这些参数的标识符。
Lambda 参数列表可以是空的,这表示 Lambda 函数不接受任何参数。在这种情况下,参数列表可以省略不写,即使用空括号()。
下面是一些Lambda参数列表的示例:
// 无参数的Lambda表达式
auto lambda1 = []() { /* 函数体 */ };
// 带有一个参数的Lambda表达式
auto lambda2 = [](int x) { /* 函数体中使用x */ };
// 带有多个参数的Lambda表达式
auto lambda3 = [](int a, double b) { /* 函数体中使用a和b */ };
// 带有默认参数的Lambda表达式(C++14及以后支持)
auto lambda4 = [](int x = 0) { /* 函数体,x有默认值 */ };
// 带有可变参数的 Lambda 表达式(通过 std::initializer_list 或模板实现)
auto lambda5 = [](std::initializer_list<int> args) { /* 函数体中使用args */ };
// 或者使用模板和完美转发实现可变参数 Lambda(更高级的用法)
在 Lambda 函数体中,可以像普通函数一样使用这些参数。Lambda 函数体是对这些参数进行操作的代码块,它出现在参数列表之后,由一对花括号{}包围。
需要注意的是,Lambda 表达式的参数列表与普通函数的参数列表一样,也需要遵循 C++ 的类型推导和参数匹配规则。如果 Lambda 表达式的参数类型可以从上下文或参数初始化中推导出来,那么可以省略参数类型,只写参数名。否则,需要显式指定参数类型。
3.2 在 Lambda 模拟使用默认参数与可变参数
在 C++11 中,Lambda 表达式虽然提供了强大的函数式编程能力,但它并不直接支持默认参数和可变参数。Lambda 表达式的参数列表与普通函数的参数列表类似,需要明确指定每个参数的类型和名称。然而,你可以通过一些技巧和方法来模拟实现类似默认参数和可变参数的效果。
默认参数
C++11 中的 Lambda 表达式本身并不直接支持默认参数(C++14 及以后支持)。这意味着不能像普通函数那样为 Lambda 表达式的参数指定默认值。但是,可以通过重载 Lambda 表达式或使用外部变量来模拟实现默认参数的效果。
一种可能的方法是定义一个包含默认值的外部变量,并在 Lambda 表达式内部检查该变量是否被显式设置。如果没有被设置,则使用默认值。这种方法并不是真正的默认参数,而是一种模拟实现。
可变参数
C++11 的 Lambda 表达式同样不支持直接的可变参数列表。可变参数列表通常用于接受不同数量和类型的参数。然而,C++ 标准库提供了 std::initializer_list 来允许函数接受任意数量的同类型参数。虽然这不是真正的可变参数模板,但可以在一定程度上模拟可变参数的效果。
如果真的需要可变参数的功能,可能需要借助模板和递归展开等技术来实现。这通常涉及到更高级的编程技巧,并且可能会使代码变得相对复杂。
示例:模拟默认参数和可变参数
下面是一个使用 std::initializer_list 来模拟可变参数的 Lambda 表达式的示例:
#include <iostream>
#include <initializer_list>
int main() {
auto lambda = [](const std::initializer_list<int>& args) {
for (const auto& arg : args) {
std::cout << arg << " ";
}
std::cout << std::endl;
};
// 使用Lambda表达式,传入不同数量的参数
lambda({1, 2, 3}); // 输出: 1 2 3
lambda({4, 5}); // 输出: 4 5
lambda({6}); // 输出: 6
}
这个例子使用了 std::initializer_list<int> 来允许 Lambda 表达式接受任意数量的整数参数。然后,通过遍历 initializer_list,可以处理每个传入的参数。
然而,需要强调的是,这种方法并不是真正的可变参数模板,因为它要求所有参数都是同一类型。对于真正的可变参数模板(即可以接受不同类型和数量的参数),需要使用 C++11 中的模板元编程技术,这通常涉及到更复杂的编程概念,如递归模板展开和类型萃取等。
4 Lambda 表达式的返回类型
Lambda 表达式的返回类型可以是显式指定的,也可以由编译器自动推导。Lambda 表达式的返回类型决定了 Lambda 函数体中的 return 语句所返回值的类型。如下是 Lambda 表达式的各种返回类型:
(1)显式指定返回类型
可以通过在 Lambda 表达式的参数列表后面添加->和返回类型来显式指定 Lambda 表达式的返回类型。这种方式提供了最大的明确性和控制力,这种方式可以精确地指定 Lambda 函数应该返回的类型。
auto lambda = [](int x, int y) -> int {
return x + y;
};
在这个例子中,Lambda 表达式显式指定了返回类型为 int。因此,Lambda 函数体内的 return 语句必须返回一个 int 类型的值。
(2)自动推导返回类型
如果不显式指定 Lambda 表达式的返回类型,编译器会根据 Lambda 函数体内的 return 语句来自动推导返回类型。这种方式更加简洁,但要求 Lambda 函数体内的所有 return 语句都必须返回相同类型的值。
auto lambda = [](int x, int y) {
return x + y; // 编译器会推导出返回类型为int
};
这个例子没有显式指定返回类型,编译器根据 return 语句 return x + y;推导出返回类型为 int。
(3)void返回类型
如果 Lambda 函数体中没有 return 语句,或者所有的 return 语句都没有返回值(即它们是 return; 的形式),那么 Lambda 表达式的返回类型将被推导为 void。
auto lambda = []() {
// 没有return语句,返回类型为void
};
(4)捕获列表和返回类型的关系
需要注意的是,捕获列表(capture list)和返回类型在 Lambda 表达式中是独立的。捕获列表决定了 Lambda 表达式可以访问哪些外部变量,而返回类型则决定了 Lambda 函数返回值的类型。这两者互不影响。
(5)注意事项
- 如果 Lambda 函数体中有多个 return 语句,并且它们返回的类型不同,那么在不显式指定返回类型的情况下,编译器将无法自动推导返回类型,并会报错。
- 在某些情况下,即使 Lambda 函数体中没有显式的 return 语句,编译器也可能推导出非 void 的返回类型,例如当 Lambda 函数体包含抛出异常的表达式时。这种情况下,Lambda 表达式的返回类型将是该异常的类型。
5 mutable 限定符
mutable 限定符用于允许在 Lambda 函数体内修改按值捕获的外部变量。默认情况下,按值捕获的变量在 Lambda 函数体内是不可修改的,因为它们是作为副本存在的。但是,当使用了 mutable 限定符后,这些变量就可以被修改了。
下面是一个使用 mutable 限定符的 Lambda 表达式的例子:
int main() {
int x = 10;
auto lambda = [x](int y) mutable {
x += y; // 由于使用了mutable限定符,这里可以修改按值捕获的x
return x;
};
std::cout << lambda(5) << std::endl; // 输出15,因为x被修改为15了
return 0;
}
在这个例子中,变量 x 是按值捕获的,但是由于 Lambda 表达式使用了 mutable 限定符,因此在 Lambda 函数体内可以修改 x 的值。
注意事项
-
谨慎使用 mutable:虽然 mutable 提供了修改按值捕获变量的能力,但这也可能引入一些意料之外的行为,特别是当 Lambda 表达式被传递给其他函数或存储在容器中时。因此,在使用 mutable 时应该谨慎,确保理解其带来的后果。
-
捕获方式的影响:mutable 限定符只影响按值捕获的变量。对于按引用捕获的变量,它们在 Lambda 函数体内本来就是可以修改的,无需使用 mutable。
-
性能考虑:按值捕获大型对象或数组时,使用 mutable 并不会改变性能开销。性能问题主要来自于对象的复制操作,而不是 mutable 本身。
6 在类中使用 Lambda 表达式
在 C++11 中,Lambda 表达式可以非常方便地在类的成员函数中定义和使用,为类的功能扩展提供了极大的灵活性。Lambda 表达式可以在类的成员函数内部定义,并且可以作为函数对象(即具有 operator() 的重载的对象)被传递和存储。下面详细讲解在类中使用 Lambda 表达式的几个方面:
(1)在成员函数内部定义 Lambda 表达式
Lambda 表达式可以直接在类的成员函数内部定义,其定义方式和在普通函数或全局作用域中定义类似。Lambda 表达式可以捕获类的成员变量或函数参数,以在 Lambda 函数体内使用。
class MyClass {
public:
void someMemberFunction() {
int localVar = 42;
auto lambda = [this, localVar]() {
// 在这里可以访问类的成员变量和成员函数
std::cout << memberVar << std::endl;
std::cout << localVar << std::endl;
};
// 调用Lambda表达式
lambda();
}
private:
int memberVar = 10;
};
在上面的例子中,someMemberFunction 成员函数内部定义了一个 Lambda 表达式 lambda,它捕获了类的成员变量 memberVar 和局部变量 localVar。Lambda 表达式通过 [this, localVar] 捕获列表捕获了这些变量,并在函数体内打印它们的值。
(2)使用Lambda表达式作为回调函数
Lambda 表达式常常作为回调函数传递给类的其他成员函数或类的外部函数。这种情况下,Lambda 表达式通常作为函数对象被传递。
#include <vector>
#include <functional>
class MyClass {
public:
void processData(const std::vector<int>& data, std::function<void(int)> callback) {
for (int value : data) {
// 调用回调函数处理数据
callback(value);
}
}
void doSomething() {
std::vector<int> data = {1, 2, 3, 4, 5};
// 使用Lambda表达式作为回调函数
processData(data, [this](int value) {
std::cout << "Processing value: " << value << std::endl;
// 在这里可以访问类的成员变量和成员函数
this->someMemberFunction(value);
});
}
private:
void someMemberFunction(int value) {
// 处理数据...
}
};
在上面的例子中,processData 成员函数接受一个数据向量和一个回调函数作为参数。在 doSomething 成员函数中,创建了一个 Lambda 表达式并将其作为回调函数传递给 processData。Lambda 表达式内部可以访问类的成员变量和成员函数。
(3)将Lambda表达式存储在类的成员中
Lambda 表达式也可以被存储在类的成员变量中,以便在类的多个成员函数或不同时间点重复使用。
#include <functional>
class MyClass {
public:
MyClass() {
// 在构造函数中初始化Lambda表达式
myLambda = [this](int x) {
std::cout << "Lambda called with " << x << std::endl;
// 可以访问类的成员变量和成员函数
this->someMemberFunction(x);
};
}
void callLambda(int value) {
// 调用存储的Lambda表达式
myLambda(value);
}
private:
std::function<void(int)> myLambda;
void someMemberFunction(int value) {
// 处理数据...
}
};
在这个例子中,myLambda 是一个 std::function 类型的成员变量,用于存储 Lambda 表达式。在类的构造函数中,初始化 myLambda 为一个 Lambda 表达式。之后,在 callLambda 成员函数中,可以调用这个存储的 Lambda 表达式。
(4)注意事项
- 捕获列表:在类的成员函数中使用 Lambda 表达式时,需要特别注意捕获列表的使用。如果你需要访问类的成员变量或成员函数,通常需要使用 [this]来捕获当前对象的指针。这允许在 Lambda 函数体内通过 this 指针访问类的成员。
- 生命周期问题:存储在类成员中的 Lambda 表达式必须确保在其被调用的整个生命周期内都是有效的。特别是,如果 Lambda 表达式捕获了类的成员变量或外部对象的引用,必须确保这些对象在 Lambda 表达式被调用时仍然存在。
- 性能考虑:虽然 Lambda 表达式在类中提供了极大的灵活性,但它们也可能引入一些性能开销。特别是在频繁调用或存储大量 Lambda 表达式时,需要注意内存使用和性能影响。