c++中的Lambda表达式详解

目录

Lambda表达式语法

捕获列表的几种捕获方式

lambda表达式的实际运用场景

[STL 算法中的简化调用(最常用)](#STL 算法中的简化调用(最常用))

异步编程与多线程

回调函数和事件处理

lambda表达式中返回值的处理

[1. 显式指定返回值类型](#1. 显式指定返回值类型)

[2. 省略返回值类型,编译器自动推导返回值类型](#2. 省略返回值类型,编译器自动推导返回值类型)

使用lambda表达式的好处

极大地提升了代码的局部性和可读性

避免命名空间污染,降低心智负担

将lambda表达式赋值给变量

赋值语法

[1. 使用 auto (最常用、最简洁)](#1. 使用 auto (最常用、最简洁))

[2. 使用 std::function](#2. 使用 std::function)

好处

[1. 代码复用与可读性](#1. 代码复用与可读性)

[2. 传递性和作为函数参数](#2. 传递性和作为函数参数)


Lambda表达式语法

C++ Lambda 表达式是 C++11 标准引入的一个极其重要且方便的特性。它允许我们在需要一个函数的地方,直接就地定义一个匿名函数对象。这极大地简化了代码,不需要单独写一个函数,不需要你绞尽脑汁想函数名,直接原地生成一个匿名函数。

不用想函数命名,降低心智负担,原地下蛋,爽的一批。

编程中,特别是在与标准模板库(STL)算法配合使用时,用lambda表达式非常贴合,或者你创建线程之后,要为线程分配任务,常见的做法可能是先为线程定义一个函数,然后将这个函数传递给线程,大概率这个函数之后也用不到,为了这一个线程,相当于还要先去制作个可反复调用的磨具,完全没必要好吧。我的评价是不如直接使用一次性筷子(lambda表达式),用完就扔,不需要你洗,哈哈。

Lambda表达式的使用特点 :Lambda 表达式最常见的用法是作为可调用对象 ,将其传递给其他函数或算法,让其他函数和算法来调用它 。通常不是生成后主动调用,通常是被动调用。

C++ Lambda 表达式的完整语法如下:

捕获列表(参数列表) -> 返回类型 { 函数体 }

// -> 返回类型这部分也可以不写,是可选的,后面返回值处理那块会详细介绍

捕获列表: 这是Lambda表达式最独特的部分,它定义了Lambda函数体内部可以访问其所在作用域中的哪些外部变量。捕获列表可以是空的 \[\],也可以有多种捕获方式(后面我们详细介绍)。

(参数列表) : 和普通函数一样,这里定义了Lambda表达式接受的输入参数。如果不需要参数,可以省略括号,即 \[\] {}。

-> 返回类型 : 指定Lambda表达式的返回值类型。在大多数情况下,编译器可以根据 函数体 中的 return 语句自动推断出返回类型,因此这部分可以省略。但如果函数体有多条 return 语句且返回类型不同,或者需要返回 void 以外的复杂类型,最好显式指定。

{ 函数体 }: 包含Lambda表达式要执行的代码。

捕获列表的几种捕获方式

捕获列表是Lambda表达式的精髓,它决定了如何访问外部变量。

假设我们有以下环境:

cpp 复制代码
int x = 10;
int y = 20;
int z = 30;

1. 不捕获(\[\]):

\[\] - 空捕获列表。Lambda 内部不能访问外部作用域的任何非静态局部变量。

cpp 复制代码
auto my_lambda = [] { /* 不能访问 x, y, z */ };

常见的混淆点

有很多人会误以为my_lambda 保存的是 Lambda 表达式执行后 返回的值。事实上,my_lambda 中保存的是一个 "闭包对象",通过这个变量,可以让 对象像一个普通函数一样被多次调用。

lambda表达式本身是匿名的,这样做相当于为这个lambda表达式赋予了一个名字,之后就可以按名调用它。不过还需要注意的是:由于my_lambda这个变量具有作用域,所以我们通过这个变量调用这个函数的话,也只能在对应的作用域范围内调用,相当于一个**"局部函数"**

相比你肯定会好奇如何获取到lambda表达式的返回值,关于这部分内容,我们在文章后面获取lambda表达式返回值这部分,有详细介绍。

2. 值捕获(变量名=):

在 Lambda 创建时,将外部变量的值复制一份到 Lambda 内部。之后外部变量的改变不会影响 Lambda 内部的副本,反之亦然。

  • x, y - 按值捕获指定的变量 x 和 y。
  • = - 按值捕获所有在 Lambda 体内使用到的外部变量。
cpp 复制代码
int a = 100;
auto by_value_lambda = [a] {
    // a 在这里是 100 的一个副本
    std::cout << "Inside lambda (value copy): " << a << std::endl;
};
a = 200; // 修改外部的 a
by_value_lambda(); // 输出: Inside lambda (value copy): 100

3. 引用捕获 (\&变量名\&):

  • \&x: 以引用的方式捕获变量 x。在Lambda函数内部对 x 的修改会直接影响到外部的原始变量。
  • \&: 捕获列表中的所有外部变量都以引用的方式捕获。
cpp 复制代码
int b = 100;
auto by_ref_lambda = [&b] {
    std::cout << "Inside lambda (reference): " << b << std::endl;
    b = 300; // 修改捕获的变量
};
b = 200; // 修改外部的 b
by_ref_lambda(); // 输出: Inside lambda (reference): 200
std::cout << "Outside lambda after call: " << b << std::endl; // 输出: Outside lambda after call: 300

4. 混合捕获**:**

可以混合使用值捕获和引用捕获。

  • =, \&y, \&z - 默认按值捕获,但 y 和 z 按引用捕获。
  • \&, x, y - 默认按引用捕获,但 x 和 y 按值捕获。

lambda表达式的实际运用场景

STL 算法中的简化调用(最常用)

Lambda 广泛用于 std::sort, std::for_each, std::transform 等算法,替代传统函数对象或函数指针:

大部分需要自定义逻辑的 STL 算法,例如 std::sort, std::find_if, std::transform 等,它们期望的参数是一个 "可调用对象" (Callable)。

"可调用对象"在 C++ 中是一个广义的概念,指的是任何可以像函数一样使用 () 操作符来调用的东西。主要包括以下三种:

  1. 函数指针:这是最传统的方式,直接传递一个普通函数的地址。
  2. 函数对象:这是一个重载了函数调用运算符 operator() 的类的实例
  3. Lambda 表达式:Lambda 表达式本质上是编译器为我们自动生成的一个匿名的函数对象类。所以它天然就是一个"可调用对象
cpp 复制代码
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> nums = {3, 1, 4, 1, 5, 9};
    
    // 按降序排序
    std::sort(nums.begin(), nums.end(), [](int a, int b) { return a > b; });
    
    // 对每个元素加 1
    std::for_each(nums.begin(), nums.end(), [](int &n) { n += 1; });
    
    // 转换元素为平方
    std::transform(nums.begin(), nums.end(), nums.begin(), [](int n) { return n * n; });
    
    return 0;
}

异步编程与多线程

Lambda 作为任务传递给 std::thread 或 std::async,方便传递上下文:

cpp 复制代码
#include <thread>
#include <iostream>

int main() {
    int x = 10;
    
    // 捕获 x 的引用(注意生命周期风险!)
    std::thread t([&x]() {
        std::cout << "x in thread: " << x << std::endl;
    });
    t.join();
    
    return 0;
}

你也可以直接将一个普通函数(包括静态成员函数)的指针作为任务传递给 std::thread 或 std::async。但这样无法捕获上下文,如果任务需要访问局部变量,你必须将它们作为参数显式传递。这种方式在逻辑上也没有lambda表达式紧凑,函数定义和调用函数的地方相隔比较远,很多时候这个函数大概率只会用一次。

大多数现代 C++ 多线程编程场景中,最推荐使用 lambda 表达式!!!这样不仅简洁,而且方便的上下文捕获,Lambda 通常可以直接在调用点内联展开,这有助于编译器优化,可能会产生更高效的代码。

回调函数和事件处理

在 GUI 编程或异步 I/O 中,Lambda 作为简洁的回调:

cpp 复制代码
// 伪代码示例:按钮点击事件
button.on_click([&](Event e) {
    std::cout << "Button clicked! Value: " << e.value << std::endl;
});

lambda表达式中返回值的处理

1. 显式指定返回值类型

捕获列表(参数列表) -> 返回类型 { 函数体 }

-> 返回类型 就是 尾置返回类型,显式告诉编译器 lambda 返回什么。

推荐在返回值类型复杂或不容易推导时使用。

优点:清晰、可控;对模板 lambda 特别有用;避免类型推导歧义

2. 省略返回值类型,编译器自动推导返回值类型

如果省略 -> 返回类型:

  • 单条 return 语句 → 编译器根据这条语句推导类型。
  • 多条 return → 所有 return 的类型必须一致(或可隐式转换到同一类型)。
  • 没有 return → 返回类型是 void
cpp 复制代码
auto mul = [](int a, int b) { return a * b; }; // 推导为 int
auto half = [](int a) { return a / 2.0; };    // 推导为 double

获取lambda表达式的返回值

要获取 Lambda 表达式的返回值,你只需要像调用一个普通函数一样调用它就可以了。

1. 直接调用并获取返回值

如果你的 Lambda 表达式是临时的(没有赋值给变量),你可以直接在创建它后立即调用。

cpp 复制代码
auto result = []() {
    // 假设这个lambda表达式返回一个int
    return 42;
}();  注意这里的'()',它立即执行了前面的lambda表达式

// result 现在是int类型,值为42

注意和前面的不加()这种来进行区分,不加()的时候,变量存储的是整个lambda表达式对象。

cpp 复制代码
auto by_value_lambda = [a] {
    std::cout << "Inside lambda (value copy): " << a << std::endl;
};

其实这种创建lambda表达式,主动调用的写法比较少见,因为lambda表达式创建出的匿名函数一般都是提供给其他函数或算法被动调用

2. 通过变量调用

如果你的 Lambda 表达式已经赋值给了一个变量(比如 my_lambda),那么你通过这个变量来调用它,就像调用普通函数一样。

cpp 复制代码
// 1. 定义一个lambda表达式并赋值给变量
auto my_lambda = [](int a, int b) {
    return a + b;
};

// 2. 调用这个变量并获取返回值
int sum = my_lambda(10, 20); // 调用lambda表达式,并把返回值赋给sum

// sum 的值为 30

3. 将 Lambda 表达式作为回调函数并获取返回值

cpp 复制代码
#include <iostream>
#include <string>
#include <functional> // 引入 std::function

// 使用 std::function 作为参数类型
// std::function<std::string()> 表示一个不接受参数、返回 std::string 的可调用对象
std::string execute_task(const std::function<std::string()>& callback) {
    std::cout << "任务执行器:开始执行任务..." << std::endl;
    // 调用回调函数并获取其返回值
    std::string result = callback();
    std::cout << "任务执行器:任务执行完毕。" << std::endl;
    return result;
}

int main() {
    // 定义一个 Lambda 表达式,它与 std::function 的签名匹配
    auto my_callback_lambda = []() {
        std::cout << "回调函数:我正在执行我的逻辑..." << std::endl;
        return "任务已成功完成!";
    };

    // 将这个 Lambda 表达式传入函数
    std::string task_result = execute_task(my_callback_lambda);

    std::cout << "主函数:从任务执行器收到的结果是:'" << task_result << "'" << std::endl;

    return 0;
}

使用lambda表达式的好处

极大地提升了代码的局部性和可读性

这是最直观的优点。Lambda 允许你将一小段逻辑代码直接定义在使用它的地方

场景:对一个字符串向量,按其长度进行排序。

使用有名函数 (逻辑与使用分离)

cpp 复制代码
// 1. 在某处定义一个具名函数
bool compareByLength(const std::string& a, const std::string& b) {
    return a.length() < b.length();
}

// ... 可能在代码很远的地方 ...

// 2. 在需要的地方调用它
std::vector<std::string> names = {"Alice", "Charlie", "Bob"};
std::sort(names.begin(), names.end(), compareByLength);

为了理解 sort 的行为,你的目光必须离开 sort 这一行,去寻找 compareByLength 的定义。

可能一个函数这样写你还觉得ok,但是如果一群函数都是这样写呢?

给按钮绑事件,如果页面里 20 个按钮各干各的事,用传统写法就要起 20 个互不重复的名字;用匿名函数直接"就地解决",代码短一半,心智负担几乎为零

使用匿名函数 (Lambda) (逻辑就在眼前)

cpp 复制代码
std::vector<std::string> names = {"Alice", "Charlie", "Bob"};
std::sort(names.begin(), names.end(), [](const std::string& a, const std::string& b) {
    return a.length() < b.length();
});

排序的逻辑 a.length() < b.length() 就地可见,代码的意图一目了然,无需任何上下文切换,可读性极高。这对于简短、一次性的逻辑尤为方便。

避免命名空间污染,降低心智负担

对于那些只使用一次的、非常简单的辅助函数(比如上面例子中的 compareByLength),为它起一个名字有时是件麻烦事。如果这样的辅助函数很多,它们会充斥在你的类或全局命名空间中,造成混乱。

Lambda 是匿名的,它只在定义的作用域内有效,不会引入任何新的名称,从而保持了代码的整洁,省得你自己去想名字了。

比如你要创建一个thread线程,为这个线程分配任务,使用又名函数来定义线程执行的函数的话,你还需要根据线程执行的工作来想这个线程工作函数的命名。

将lambda表达式赋值给变量

Lambda表达式本质上是一个可调用的函数对象,它可以像普通函数一样被调用,也可以被赋值给变量。将Lambda表达式赋值给变量,可以让你在程序中更灵活地使用它,比如将其作为参数传递给其他函数,或者存储起来以备后用。

基本的赋值语法遵循 C++ 变量声明和赋值的模式:

变量类型 变量名 = 捕获列表(参数列表) -> 返回值类型 {

// 函数体

};

下面是几种常见的将 Lambda 表达式赋值给变量的语法,每种都对应不同的变量类型。

赋值语法

1. 使用 auto (最常用、最简洁)

这是最推荐的方式,编译器会自动推断出 Lambda 表达式的类型。

auto 变量名 = 捕获列表(参数列表) {

// 函数体

};

示例:

cpp 复制代码
// auto 会自动推断出 add 的类型
auto add = [](int a, int b) {
    return a + b;
};

int result = add(3, 4); // result = 7

误区避免:这里一定要知道,这里add赋值的是整个lambda表达式,是 Lambda 表达式本身所代表的"可调用对象",而不是lambda表达式的返回值。

2. 使用 std::function

std::function 是一个泛型函数封装器,可以存储任何可调用对象,包括 Lambda。当你需要将 Lambda 表达式作为函数参数传递,或者需要一个统一类型的变量来存储不同 Lambda 时,它非常有用。

cpp 复制代码
#include <functional>

std::function<返回值类型(参数类型1, 参数类型2, ...)> 变量名 = [捕获列表](参数列表) {
    // 函数体
};

适用场景&好处

1. 代码复用与可读性

将一个Lambda表达式赋值给一个变量后,这个变量就成了一个可调用的对象。你可以在代码中的不同地方多次使用这个变量来调用同一个Lambda函数,而不需要重复地写出整个Lambda表达式。这不仅减少了重复代码,也让代码逻辑更清晰。

例如:

cpp 复制代码
// 传统的做法,重复编写逻辑
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::for_each(numbers.begin(), numbers.end(), [](int i){ std::cout << i * 2 << " "; });
std::cout << std::endl;
std::for_each(numbers.begin(), numbers.end(), [](int i){ std::cout << i * 2 << " "; });

如果我们将Lambda表达式赋值给一个变量,代码会更简洁:

cpp 复制代码
auto doubleAndPrint = [](int i){ std::cout << i * 2 << " "; };
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::for_each(numbers.begin(), numbers.end(), doubleAndPrint);
std::cout << std::endl;
std::for_each(numbers.begin(), numbers.end(), doubleAndPrint);

解惑:既然这里想要lambda表达式复用,那为什么不直接写成普通函数呢?为什么要写成lambda并且还要赋值给变量呢?

实际上,将Lambda赋值给变量是为了结合两者的优点

  • 保留Lambda的捕获能力:它依然可以捕获外部变量,这是普通函数做不到的。

  • 获得普通函数的复用性 :一旦赋值给变量,你就可以在代码的不同地方多次调用它,或者作为参数传递给其他函数,而不需要重复编写整个表达式。这就像给一个简短的、有特定捕获行为的"小函数"起了一个名字,方便管理和复用。

总结一下,Lambda表达式赋值给变量,是一种在捕获外部变量代码复用之间找到的完美平衡。它让你既能享受到Lambda的灵活性,又能获得普通函数的部分可维护性。你通常会在一个简短的、需要捕获外部状态的逻辑被多次使用时,采用这种模式。

2. 传递性和作为函数参数

将Lambda表达式赋值给变量后,你可以像传递任何其他函数对象一样,将这个变量作为参数传递给函数。这在设计通用算法或回调函数时非常有用。

例如,你可以创建一个函数,它接受一个可调用对象作为参数:

cpp 复制代码
void applyOperation(std::vector<int>& vec, const std::function<void(int)>& operation) {
    for (int& i : vec) {
        operation(i);
    }
}

int main() {
    auto addTen = [](int& i){ i += 10; };
    std::vector<int> numbers = {1, 2, 3};
    applyOperation(numbers, addTen); // 将Lambda表达式作为参数传递
    // numbers 现在是 {11, 12, 13}
    return 0;
}

在这里,addTen 这个变量就像一个普通的函数一样被传递和使用,这大大提高了代码的模块化和灵活性

相关推荐
王老师青少年编程6 分钟前
信奥赛C++提高组csp-s之FHQ Treap
c++·csp·平衡树·信奥赛·csp-s·提高组·fhq treap
QiLinkOS2 小时前
《打破“用爱发电”:一种基于 Gitee 与时间戳的开源权益分配机制探索》
c语言·数据结构·c++·科技·算法·gitee·开源
Irissgwe2 小时前
c++STL--string类
c++·stl·string
Irissgwe2 小时前
c++类型转换
c++·类型转换·explicit·static_cast·const_cast·dynamic_cast·rtti
智者知已应修善业2 小时前
【51单片机用T0定时器方式1,实现0.5S的时间间隔实现第一次一个灯亮、第二次二个灯亮,直到全部灯亮,然后重复整个过程】2023-12-29
c++·经验分享·笔记·算法·51单片机
智者知已应修善业3 小时前
【51单片机4位静态数码管显示1234】2023-11-14
c++·经验分享·笔记·算法·51单片机
抓虾爪3 小时前
ST意法代理商粤科源兴丨LSM6DS3全系列现货库存,LSM6DS3TR-C当天可发
c++
妙为3 小时前
unreal engine5.7.4,创建ThirdPerson第三人称模版,类型是c++崩溃
c++·ue5·虚幻·unreal engine5
郝学胜_神的一滴3 小时前
Qt 高级开发 021:零基础吃透 QVBoxLayout 垂直布局
c++·qt
Boom_Shu3 小时前
长方形的关系
数据结构·c++·算法