C++ Lambda表达式保姆级教程(超级详细,附带实例)
现代 C++(特别是从 C++11 开始)引入和强化了多种功能,以支持更灵活和强大的编程范式,其中函数式编程特性尤为显著。
Lambda 表达式是实现函数式编程的核心工具之一,体现了 C++ 设计哲学的灵活性、表达力和效率。
在 C++ 中,Lambda 表达式的本质是通过生成一个匿名类来实现的。这个匿名类自动重载了 operator(),使其实例能够像普通函数那样被调用。这种方法的优点是 Lambda 表达式既可以捕获周围作用域中的变量,实现闭包功能,又能保持与 C++ 对象模型的一致性,利用类的特性(如状态保持和成员访问)。
Lambda 表达式的设计思想源自函数式编程,强调无状态和不可变数据的操作。通过使用 Lambda 表达式,C++ 程序员可以编写出更清晰、简洁的代码,尤其在使用 STL 算法时。
C++ 的 Lambda 表达式通过捕获列表、参数列表、返回类型和函数体的组合,提供了对闭包(即捕获外部变量的函数)的支持。
编译器对 Lambda 表达式的优化如下:

图 1 Lambda的语法结构
下面介绍不同语法形式的 Lambda 表达式:
C++23 对 Lambda 表达式的扩展和改进,体现了 C++ 标准的持续进化,旨在提供更强大、灵活的编程机制。
通过这些改进,C++ 开发者可以编写更简洁、高效、易于理解的代码,尤其在需要匿名函数、泛型编程和元编程的场景中。这些特性的引入,进一步加强了 C++ 作为一个现代、高效的编程语言的地位。
这些语法形式使得 Lambda 表达式不仅能够用于简单的场景(如作为小型函数传递),还能支持复杂的泛型编程和模板元编程等高级用途。
例如,在 C++11 中,可以这样写 Lambda 表达式:
随后,这种后置返回类型的语法被扩展到普通函数中,这在 C++14 标准中得到了更广泛的支持。
在 C++14 和更高版本中,后置返回类型可以用于普通函数和模板函数,使得编写泛型代码(如模板)更加灵活和清晰。例如:
捕获列表还可以混合使用这些捕获模式,根据实际需要灵活选择。0 个或多个捕获的以逗号分隔的列表,可选择以捕获默认值开头。
Lambda 表达式还可以通过捕获列表捕获一定范围内的变量:
例如,假设有一个书本信息的列表,想要找出列表内标题中包含某个关键字(target)的书本的数量:
此外,示例展示了 C++14 中 Lambda 表达式的列表初始化功能,它允许在 Lambda 表达式中创建新变量。这种方法对于捕获外部变量而不改变其原有名字非常有用。
在按值和按引用捕获中,分别用 [v = target] 和 [&r = target] 语法展示了如何在 Lambda 捕获列表中使用初始化器,这使得在 Lambda 内部可以使用 v 和 r 这两个新变量名来引用 target。
这种方法可以在不改变外部变量名称的情况下在 Lambda 内部使用不同的变量名称,从而提高代码的可读性和灵活性。
使用 Lambda 表达式的优势在于:
接下来将通过一系列具体的例子来展示 Lambda 表达式在简化算法、容器操作和异步编程等方面的强大能力,以及它们如何提升代码的可读性、性能和可维护性。
例如,对一个整数向量进行降序排序,可以使用 Lambda 表达式自定义排序规则:
例如,使用 std::for_each 遍历一个向量,并将其中的每个元素加倍:
例如,使用 std::async 启动一个异步任务来计算斐波那契数列的第 n 项:
例如,下面的代码定义了一个高阶函数 compose(),用于组合两个函数:
通过这种方式,我们可以组合任意两个函数,创建出新的功能。在测试部分,使用 square_then_increment() 函数先对 3 进行平方运算,然后将结果加 1,最终得到 10。
这个示例展示了 Lambda 表达式在函数式编程中的强大能力,特别是在组合函数时的灵活性。
例如,使用 Lambda 表达式实现一个惰性求和函数:
这种惰性求值的方法在处理大量数据或执行代价较高的计算时非常有用,因为它可以避免不必要的计算,提高程序的效率。
以上高级用法展示了 Lambda 表达式在实际编程中的强大潜力,它们有助于我们编写出更简洁、高效的代码。当然,这些技巧只是 Lambda 表达式的冰山一角,掌握这些高级用法,将帮助我们更好地发挥 Lambda 表达式的威力。
Lambda 表达式是实现函数式编程的核心工具之一,体现了 C++ 设计哲学的灵活性、表达力和效率。
在 C++ 中,Lambda 表达式的本质是通过生成一个匿名类来实现的。这个匿名类自动重载了 operator(),使其实例能够像普通函数那样被调用。这种方法的优点是 Lambda 表达式既可以捕获周围作用域中的变量,实现闭包功能,又能保持与 C++ 对象模型的一致性,利用类的特性(如状态保持和成员访问)。
Lambda 表达式的设计思想源自函数式编程,强调无状态和不可变数据的操作。通过使用 Lambda 表达式,C++ 程序员可以编写出更清晰、简洁的代码,尤其在使用 STL 算法时。
C++ 的 Lambda 表达式通过捕获列表、参数列表、返回类型和函数体的组合,提供了对闭包(即捕获外部变量的函数)的支持。
编译器对 Lambda 表达式的优化如下:
- 内联展开:对于简单的 Lambda 表达式(无论是否捕获变量),编译器可能会将它内联展开,就像对普通函数的内联优化一样。这意味着在调用点,会直接插入 Lambda 表达式的代码,而不是进行函数调用,这种优化减少了函数调用的开销。
- 转换为函数指针:如果 Lambda 表达式没有捕获任何外部变量,它可以被转换为一个函数指针。这是因为没有捕获的 Lambda 表达式不需要维持任何状态,所以它的行为更接近于普通函数。编译器可以利用这一点,在某些情况下将这样的 Lambda 表达式转换为等价的函数指针,从而进一步优化代码。
注意,优化取决于编译器的实现和具体情况,而非语言规范强制定义的行为。
C++ Lambda语法结构
Lambda 语法结构如下图所示:
图 1 Lambda的语法结构
下面介绍不同语法形式的 Lambda 表达式:
1、基于不显式模板参数列表的Lambda表达式
[captures](params) specs(exception) back-attr(trailing-type) requires { body }参数说明:
- captures:捕获列表,定义哪些外部变量被 Lambda 表达式捕获以及如何捕获(值捕获或引用捕获)。
- params:参数列表,定义 Lambda 表达式接收的参数。
- specs:指定 Lambda 表达式的属性,如 mutable、constexpr 等(可选)。
- exception:异常规范,指定 Lambda 表达式可以抛出的异常类型(可选)。
- back-attr:后置属性(可选)。
- trailing-type:返回类型后置语法(可选)。
- requires:约束表达式,用于模板 Lambda 表达式(可选)。
- body:Lambda 表达式的函数体。
2、不带参数列表的Lambda表达式
[captures] { body }最简单的 Lambda 表达式形式,仅包含捕获列表和函数体。
3、Lambda表达式扩展语法(自C++20起)
带显式模板参数列表的 Lambda 表达式(总是泛型):[captures]<tparams> t-requires(front-attr)(params) specs(exception) back-attr(trailing-type) requires { body }参数说明:
- tparams:模板参数列表,使 Lambda 表达式支持泛型编程;
- t-requires:模板约束,定义模板参数需要满足的要求(可选)。
4、C++23新视角
1) Lambda表达式的简化语法
C++23 引入了一些新的语法特性,使得编写 Lambda 表达式更加灵活和简洁。这包括但不限于以下几点:- [captures] { body } 和 [captures] (params) { body } 的形式保持不变,依然是 Lambda 表达式的核心;
- 对于不需要参数列表的 Lambda 表达式,C++23 之前需要写作 [captures] () { body },现在可以简化为 [captures] { body },即省略空的参数列表。
2) 后置返回类型和异常规范
C++23 允许在 Lambda 表达式中更灵活地使用后置返回类型和异常规范,以便更清晰地指定 Lambda 函数的行为和类型:- Lambda 表达式可以包含 trailing-return-type(后置返回类型),使得返回类型的指定更加灵活,尤其在返回类型较复杂或需要依赖参数类型的场景中;
- 异常规范(如 noexcept)的使用使得 Lambda 表达式可以显式地声明其是否会抛出异常,有助于编写更安全、明确的代码。
3) 模板Lambda表达式的增强
自 C++20 以来,Lambda 表达式支持模板参数,C++23 进一步增强了这一功能:- 模板 Lambda 表达式可以更灵活地定义泛型代码,通过在捕获列表之后使用模板参数列表 <tparams> 来实现。这使得 Lambda 表达式可以像模板函数一样,根据传入的参数类型进行自动实例化和类型推导。
- requires 子句的支持允许对模板 Lambda 表达式的模板参数进行约束,进一步提升了泛型编程的能力和灵活性。
4) 属性和规范的扩展
C++23 增加了对 Lambda 表达式中使用属性和规范的支持,包括:- [[attributes]] 可以应用于 Lambda 表达式,允许开发者指定编译器特定的优化或行为指示,例如 [[nodiscard]]、[[maybe_unused]] 等;
- constexpr 和 consteval Lambda 表达式的支持,允许在编译时求值,增强了编译时计算和元编程的能力。
C++23 对 Lambda 表达式的扩展和改进,体现了 C++ 标准的持续进化,旨在提供更强大、灵活的编程机制。
通过这些改进,C++ 开发者可以编写更简洁、高效、易于理解的代码,尤其在需要匿名函数、泛型编程和元编程的场景中。这些特性的引入,进一步加强了 C++ 作为一个现代、高效的编程语言的地位。
这些语法形式使得 Lambda 表达式不仅能够用于简单的场景(如作为小型函数传递),还能支持复杂的泛型编程和模板元编程等高级用途。
5) 返回值后置的引入
值得一提的是,在 C++ 中,返回类型后置(也称为尾返回类型或后置返回类型)最初是在 C++11 标准中引入的,主要用于 Lambda 表达式。这种语法允许在 Lambda 表达式中清晰地指定返回类型,尤其在自动类型推导不适用或者需要明确指定类型的情况下。例如,在 C++11 中,可以这样写 Lambda 表达式:
auto func = []() -> int { return 42; };这里的“-> int”是后置返回类型,指定了 Lambda 表达式的返回类型为 int。
随后,这种后置返回类型的语法被扩展到普通函数中,这在 C++14 标准中得到了更广泛的支持。
在 C++14 和更高版本中,后置返回类型可以用于普通函数和模板函数,使得编写泛型代码(如模板)更加灵活和清晰。例如:
template<typename T, typename U> auto add(T x, U y) -> decltype(x + y) { return x + y; }在这个例子中,decltype(x + y)用于推导 x 和 y 相加的结果类型,这是在编译时自动推断的。
C++ Lambda捕获方式
捕获列表支持多种捕获模式,包括值捕获、引用捕获、隐式值捕获和隐式引用捕获:- 值捕获是以传值方式捕获变量,这意味着在 Lambda 表达式中使用的是变量的副本;
- 引用捕获是以传引用方式捕获变量,这意味着在 Lambda 表达式中使用的是变量的引用;
- 隐式值捕获和隐式引用捕获则可以一次性捕获所有变量,分别使用“=”和“&”表示。
捕获列表还可以混合使用这些捕获模式,根据实际需要灵活选择。0 个或多个捕获的以逗号分隔的列表,可选择以捕获默认值开头。
Lambda 表达式还可以通过捕获列表捕获一定范围内的变量:
捕获方式 | 说明 |
---|---|
[] | 不捕获任何变量。 |
[&] | 捕获外部作用域中的所有变量,并作为引用在函数体中使用(按引用捕获)。 |
[=] | 捕获外部作用域中的所有变量,并作为副本在函数体中使用(按值捕获)。 |
[=,&foo] | 按值捕获外部作用域中的所有变量,并按引用捕获 foo 变量。 |
[a, &b] | 以值的方式捕获 a,以引用的方式捕获 b,也可以捕获多个。 |
[bar] | 按值捕获 bar 变量,同时不捕获其他变量。 |
[this] | 捕获当前类中的 this 指针,让 Lambda 表达式拥有和当前类成员函数同样的访问权限。如果已经使用了 & 或者 =,就默认添加此选项。捕获 this 的目的是可以在 Lambda 中使用当前类的成员函数和成员变量。 |
例如,假设有一个书本信息的列表,想要找出列表内标题中包含某个关键字(target)的书本的数量:
#include <algorithm> #include <vector> #include <string> #include <iostream> // 定义一个图书的结构体,包含书籍的ID、标题和价格 struct Book { int id; std::string title; double price; }; int main() { // 初始化一些图书 std::vector<Book> books = { {1, "C++ Primer", 45.95}, {2, "Effective Modern C++", 54.99}, {3, "The C++ Programming Language", 59.95} }; // 要搜索的目标字符串 std::string target = "C+"; // 使用 Lambda 按值捕获 target,并使用列表初始化在 Lambda 内部创建新变量 v auto count_by_value = [&books, v = target]() { return std::count_if(books.begin(), books.end(), [v](const Book& book) { return book.title.find(v) != std::string::npos; }); }; // 使用 Lambda 按引用捕获 target,并使用列表初始化在 Lambda 内部创建新变量 r auto count_by_reference = [&books, &r = target]() { return std::count_if(books.begin(), books.end(), [&r](const Book& book) { return book.title.find(r) != std::string::npos; }); }; // 输出按值捕获和按引用捕获的计数结果 std::cout << "Count by value: " << count_by_value() << std::endl; std::cout << "Count by reference: " << count_by_reference() << std::endl; return 0; }在这个示例中,使用了 Book 结构体的全部属性,即 id、title 和 price。
此外,示例展示了 C++14 中 Lambda 表达式的列表初始化功能,它允许在 Lambda 表达式中创建新变量。这种方法对于捕获外部变量而不改变其原有名字非常有用。
在按值和按引用捕获中,分别用 [v = target] 和 [&r = target] 语法展示了如何在 Lambda 捕获列表中使用初始化器,这使得在 Lambda 内部可以使用 v 和 r 这两个新变量名来引用 target。
这种方法可以在不改变外部变量名称的情况下在 Lambda 内部使用不同的变量名称,从而提高代码的可读性和灵活性。
C++ Lambda表达式实例
Lambda 表达式的使用场景包括但不限于:替换小型函数、简化 STL 算法和函数适配器、实现回调函数和事件处理,以及简化并行和异步编程。使用 Lambda 表达式的优势在于:
- 简化语法,提高代码的可读性和可维护性:不需要额外再写一个函数或者函数对象,避免了代码膨胀和功能分散,使开发者更加集中精力于手边的问题,同时也获取了更高的生产率;
- 更好的性能,编译器可以更好地进行内联优化;
- 声明式编程风格:就地匿名定义目标函数或函数对象,减少代码冗余;
- 以更直接的方式去写程序,更好的可读性和可维护性:更好地支持函数式编程范式,使代码更加通用和可复用;
- 在需要的时间和地点实现功能闭包,使程序更具灵活性。
接下来将通过一系列具体的例子来展示 Lambda 表达式在简化算法、容器操作和异步编程等方面的强大能力,以及它们如何提升代码的可读性、性能和可维护性。
1) 使用Lambda表达式简化算法
C++ 标准库中包含许多算法,如 sort、for_each、transform 等,使用 Lambda 表达式可以使这些算法更加简洁和灵活。例如,对一个整数向量进行降序排序,可以使用 Lambda 表达式自定义排序规则:
#include <algorithm> #include <vector> #include <iostream> int main() { std::vector<int> numbers = {3, 1, 4, 1, 5, 9, 2, 6, 5}; std::sort(numbers.begin(), numbers.end(), [](int a, int b) { return a > b; }); for (int num : numbers) { std::cout << num << " "; } std::cout << std::endl; return 0; }
2) 在容器操作中使用Lambda表达式
Lambda 表达式可以与 C++ 标准库中的容器结合使用,实现更加简洁和高效的容器操作。例如,使用 std::for_each 遍历一个向量,并将其中的每个元素加倍:
#include <algorithm> #include <vector> #include <iostream> int main() { std::vector<int> numbers = {1, 2, 3, 4, 5}; std::for_each(numbers.begin(), numbers.end(), [](int &n) { n *= 2; }); for (int num : numbers) { std::cout << num << " "; } std::cout << std::endl; return 0; }
3) 异步编程与Lambda表达式
在异步编程中,Lambda 表达式可以作为回调函数或任务,简化异步任务的创建和调度。例如,使用 std::async 启动一个异步任务来计算斐波那契数列的第 n 项:
#include <future> #include <iostream> int main() { auto fibonacci = [](int n) { int a = 0, b = 1; for (int i = 0; i < n; ++i) { int temp = a; a = b; b = temp + b; } return a; }; std::future<int> result = std::async(std::launch::async, fibonacci, 10); int value = result.get(); // 获取异步任务的结果 std::cout << "Fibonacci(10) = " << value << std::endl; return 0; }
C++ Lambda表达式的高级用法
1) Lambda表达式中的条件表达式
Lambda 表达式可以使用条件表达式进行复杂的逻辑判断,例如实现多种排序规则:#include <algorithm> #include <vector> #include <iostream> int main() { // 定义一个Lambda表达式,根据参数ascending决定是升序还是降序排序 auto custom_sort = [](bool ascending) { return [ascending](int a, int b) { return ascending ? a < b : a > b; }; }; std::vector<int> numbers = {3, 1, 4, 1, 5, 9, 2, 6, 5}; // 使用custom_sort (true)进行升序排序 std::sort(numbers.begin(), numbers.end(), custom_sort(true)); std::cout << "Ascending order: "; for (int num : numbers) { std::cout << num << " "; } std::cout << std::endl; // 使用custom_sort (false)进行降序排序 std::sort(numbers.begin(), numbers.end(), custom_sort(false)); std::cout << "Descending order: "; for (int num : numbers) { std::cout << num << " "; } std::cout << std::endl; return 0; }在这个示例中,custom_sort 是一个接收布尔参数 ascending 的 Lambda 表达式,根据这个参数返回一个新的 Lambda 表达式,用于升序或降序排序。然后,使用 std::sort() 函数和 custom_sort() 来对 numbers 向量进行排序,并分别打印升序和降序的结果。
2) 嵌套Lambda表达式
Lambda 表达式可以嵌套在其他 Lambda 表达式中,以实现更高级的功能。例如,下面的代码定义了一个高阶函数 compose(),用于组合两个函数:
#include <iostream> int main() { // 定义一个高阶函数compose,用于组合两个函数 auto compose = [](auto f1, auto f2) { return [f1, f2](auto x) { return f1(f2(x)); }; }; // 定义两个简单的函数:square和increment auto square = [](int x) { return x * x; }; auto increment = [](int x) { return x + 1; }; // 使用compose组合square和increment函数 auto square_then_increment = compose(increment, square); // 测试组合后的函数 int result = square_then_increment(3); // 结果为10 (3 * 3 + 1) std::cout << "Result: " << result << std::endl; return 0; }在这个示例中,compose() 是一个高阶函数,它接收两个函数 f1 和 f2 作为参数,并返回一个新的 Lambda 表达式,该表达式首先应用 f2,然后将结果传递给 f1。
通过这种方式,我们可以组合任意两个函数,创建出新的功能。在测试部分,使用 square_then_increment() 函数先对 3 进行平方运算,然后将结果加 1,最终得到 10。
这个示例展示了 Lambda 表达式在函数式编程中的强大能力,特别是在组合函数时的灵活性。
3) 使用Lambda表达式实现惰性求值
Lambda 表达式可以用于实现惰性求值,即仅在需要结果时才进行计算。例如,使用 Lambda 表达式实现一个惰性求和函数:
#include <vector> #include <numeric> #include <iostream> int main() { // 定义一个Lambda表达式lazy_sum,用于实现惰性求和 auto lazy_sum = [](auto container) { return [container]() { return std::accumulate(container.begin(), container.end(), 0); }; }; std::vector<int> numbers = {1, 2, 3, 4, 5}; // 创建一个惰性求和函数 auto sum = lazy_sum(numbers); // 在其他操作后,当需要结果时,才进行求和计算 // 其他操作 int result = sum(); // 执行求和计算 std::cout << "Sum: " << result << std::endl; return 0; }在这个示例中,lazy_sum 是一个返回 Lambda 表达式的函数,这个 Lambda 表达式捕获了容器 container,并在调用时才执行求和操作。通过这种方式,可以延迟求和计算,直到真正需要结果时再进行计算。
这种惰性求值的方法在处理大量数据或执行代价较高的计算时非常有用,因为它可以避免不必要的计算,提高程序的效率。
以上高级用法展示了 Lambda 表达式在实际编程中的强大潜力,它们有助于我们编写出更简洁、高效的代码。当然,这些技巧只是 Lambda 表达式的冰山一角,掌握这些高级用法,将帮助我们更好地发挥 Lambda 表达式的威力。