# 纲要 > 主干纲要、Hint/线索/路标 - lambda 表达式 - lambda 的语法形式 - lambda 的捕获列表 - 可捕获目标 - 捕获方式 - 初始化捕获:可在捕获列表中使用 "**初始化表达式**",甚至捕获移动对象。 - lambda 的返回类型 - lambda 使用示例 - lambda 实现递归 - lambda 作为函数返回类型 - 泛型 lambda(C++14) - 全局 lambda(C++17) %% # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** %% <br><br><br> # lambda 表达式 lambda 表达式是定义一个 "**匿名函数对象**"(Functor) 的语法糖。 ### 闭包 闭包(closure)是指由 lambda 表达式生成的**匿名函数对象**。 通过 `[](){...}` 语法声明一个 "**lambda 表达式**" 时,编译器完成以下工作: 1. 生成一个唯一的 **==匿名类类型==**,称之为 "**==闭包类==**"。 2. 实例化该匿名类的一个 **临时==函数对象==**,称之为 "**==闭包==**"。 因此,`auto f = [](){...};` 即是**为 lambda 表达式生成的闭包对象** 赋予一个变量名。 在闭包类中: - lambda 函数体作为 **函数调用运算符的重载**:`operator()() const { ... }` - 捕获列表捕获的变量会作为 "**匿名类**" 的数据成员(按值捕获是 "**拷贝副本**",按引用捕获则是 "**引用**") - 编译器自动生成相应的构造函数,初始化其数据成员。 <br><br> ## lambda 表达式的语法形式 > ![[_attachment/02-开发笔记/01-cpp/函数相关/cpp-lambda 表达式.assets/IMG-cpp-lambda 表达式-F9344B27771318514B954FDC20CBCE84.png|598]] lambda 表达式的语法形式包括以下部分: - **捕获列表** - **参数列表**(无参数时可省略,支持指定 **默认参数**) - **函数修饰符**(可选):`mutable` 与 `noexcept` - **尾置返回类型**(可省略,由编译器自动推断) - **函数体** ```cpp // lambda表达式可省略参数列表和返回类型,但必须声明捕获列表和函数体 auto f = [] { return 42 }; // 无参数的lambda cout << f() << endl; ``` <br><br><br> # lambda 的捕获列表 > ![[_attachment/02-开发笔记/01-cpp/函数相关/cpp-lambda 表达式.assets/IMG-cpp-lambda 表达式-D893AD586CA0A247E3B17209C3852F20.png|690]] <br> ## 可捕获目标 捕获列表仅用以获取 "**lambda 创建时所在的外层作用域下的==非静态局部变量==**"。 **==全局变量==**、**==全局静态变量==**、**lambda 创建时所在作用域的==局部静态变量==**,均可在 lambda 函数体内直接访问,无需捕获。 > [!caution] lambda 只能捕获其被创建时 "**所在作用域里的==非静态局部变量==**"(包括形参) > > 在类成员函数中,默认按值捕获时`[=]`,捕获的是 "this" 指针,而非类的数据成员[^1]。 > > ```cpp > using FilterContainer = std::vector<std::function<bool(int)>>; > FilterContainer filters; // 一个全局容器, 存放过滤函数 > > class MyClass { > public: > ... > void alddFilter() const; > private: > int divisor; > }; > > void MyClass::addFileter() const { > filters.emplace_back( > [=](int value) { return value % divisor == 0; } > ); > // 按值捕获`=`真正捕获的是"this"指针, lambda 函数体内的 divisor 其实是 this->divisor. > // 一旦一个类实例被销毁, 则存入 filters 中的闭包里的 this 指针就会失效! > // > // 如果尝试显示捕获 divisor, 捕获列表改为`[divisor]`, 就会编译失败. > // 因为 divisor 并不是"局部变量", 而是类的成员变量,类的成员无法被直接捕获。 > } > ``` > > [!caution] lambda 的 "函数形参" 与 "捕获变量" 同名时,后者将被隐藏! > > 该情况下,函数体内只能访问到 "同名形参"! > > ```cpp > int main() { > int x = 100; > auto lambda = [x](int x) { > cout << "x inside lambda: " << x << endl; // 函数体内, 同名形参会隐藏"捕获的外层作用域下同名变量", 无法访问后者. > }; > lambda(42); // 输出 42,而不是 100 > } > ``` > > [!caution] C++20 之前,不支持 lambda 捕获外层作用域中通过 "**==结构化绑定==**" 声明的变量! > > ```cpp > auto [x, y] = pii; > > auto f = [x, y]() { // Warning: Captured structed bindings are a C++20 extension > cout << x << " " << y << endl; > }; > ``` > > > [!error] 静态数组名被 "默认捕获" 时,退化为 "指向数组元素类型的指针",不能应用结构化绑定! > > ![[_attachment/02-开发笔记/01-cpp/函数相关/cpp-lambda 表达式.assets/IMG-cpp-lambda 表达式-7E95FE7BADB256FCAD16C6D6A138AE36.png|579]] > > 解决办法:用 `&dirs` 显式捕获数组名 > > ```cpp > auto dfs = [&dirs](auto& self) { // `&dirs` 显式指定捕获数组名, 不会退化 > for (auto [dx, dy] : dirs) { > cout << dx << dy << endl; > } > }; > ``` > <br><br> ## 捕获方式 lambad 支持两种捕获方式: - **按值捕获**: 在 "lambda 创建时" **拷贝捕获变量的 "==副本=="**,作为闭包的数据成员。 - **按引用捕获** :在 "lambad 创建时" 建立**对捕获变量的 "==引用=="**,作为闭包的数据成员。 > [!caution] "按值捕获" 的变量在 lambda 函数体内默认不可修改 > > 本质原因:**lambda 表达式生成的闭包类的函数调用运算符的重载默认为 `operator()() const`**,参见 [^2] [^3] > > 若要在函数体内修改 "**按值捕获**" 的副本,需 **==声明 `mutable` 函数修饰符==**。 > > ```cpp > int x = 10; > auto lambda = [x]() mutable { > x += 1; // lambda 必须声明为`mutable`, 否则不可修改"按值捕获" 的值. > }; > ``` > > [!NOTE] "按引用捕获" 的变量在 lambda 函数体内可被直接修改 > > 本质原因:**类的 const 成员函数中本身就可修改 "==引用成员=="**,如下所示: > > ```cpp > class FuncObj { > public: > FuncObj(int v, int& r) : var(v), ref(r) {} > > void operator()() const { > // var = 42; // 错误: const 成员函数内, 不能修改"值类型的成员变量" > ref = 42; // 正确: const 成员函数内, 可以修改"引用类型的成员变量", > // 原因在于修改的不是"引用这一成员本身", 而是"成员所引用的外部对象". > // 编译器在实现时, 对"引用成员"实际实现为"指针", 存储的是被引用对象的地址. > // 因此, 在 const 成员函数内"修改引用", 等价于"修改指针所指对象", 而非"修改指针本身", 故可编译通过. > } > > private: > int var; > int& ref; > }; > ``` > > 说明示例: ```cpp int x = 10, y = 20; // 捕获变量x, 通过值捕获, x的副本被存储在lambda函数对象中; auto lambda1 = [x] { /* 使用x; 但不能修改值捕获的x */ }; // 通过"值捕获"得到的变量, 如果要在lambda内修改其值, 则必须在参数列表后加上关键字mutable; // 值捕获下, 内部对x的修改不会影响外部作用域的x; auto lambda11 = [x]() mutable { return ++x; } // ✔ // 捕获变量x, 引用捕获, lambda内部引用外部作用域的x,lambda内部对x的修改即是对外部x的修改; auto lambda2 = [&x] { /* lambda内部引用外部作用域的x; */ }; // 通过值捕获x, 通过引用捕获y; auto lambda3 = [x, &y] { /* 使用x和y, */ }; // 隐式捕获, 让编译器根据lambda函数体中的代码自动推导需要捕获的变量 ↓↓↓ // 通过值捕获使用外部任意变量 auto lambda4 = [=] { /* 通过值捕获的形式使用任意外部变量 */}; // 通过引用捕使用外部任意变量 auto lambda5 = [&] { /* 通过引用捕获的形式使用任意外部变量*/}; // 混合捕获模式(捕获列表中的首项必须是"="或"&", 表示默认捕获方式) auto lambda6 = [=, &x] { /*对x引用捕获, 而对其他所有变量进行值捕获 */ }; auto lambda7 = [&, x] { /*对x值捕获, 而对其它所有变量进行引用捕获 */} auto lambda8 = [this] { /*捕获当前类中的this指针,如果已经使用了&或者=就默认添加此选项*/ }; ``` <br><br> ## 初始化捕获(C++14) > 初始化捕获,也称 "**通用 lambda 捕获**"(generalized lambda capture) C++14 起支持 "**初始化捕获**",可在捕获列表中 "**显式==定义并初始化==指定变量**",而不仅是捕获既有的外层局部非静态变量。 具体支持三种形式[^4]: - **按值** 初始化捕获:`[变量名 = 初始化表达式]` - **按引用** 初始化捕获: `[&变量名 = 初始化表达式]` - **移动捕获**:`[变量名 = std::move(外部变量)]` 或 `[变量名 = 纯右值外部变量]` 说明示例: ```cpp int x = 42; auto lambda = [value = x]() { // 按值捕获 x: 拷贝给 lambda 内部变量 value. reurn value + 1; }; auto lambda = [&value = x]() { // 按引用捕获 x: 赋给 lambda 内部引用 value return value + 1; }; auto lambda = [vec = std::move(v)]() { // 将外部 v 移动赋值给 lambda 内部 vec. ... }; auto lambda = [ptr = std::make_unique<Widget>()] { // 构造一个临时量, 传递给ptr (移动捕获) ... }; ``` 初始化捕获的常用场景:向 lambda 函数体内传递 `unique_ptr` ```cpp auto ptr = std::make_unique<int>(10); auto f = [p = std::move(ptr)] { // 捕获列表以移动语义接收外层作用域的unique_ptr ffunc(std::move(p)); }; ``` <br> #### 初始化捕获的实现方式 在不支持 **移动捕获** 的 C++11 中,有两种方式可手动实现该功能: - (1)**使用 bind 绑定被 "移动" 对象**,作为参数传递; - (2)自定义 "**仿函数**" ```cpp // 使用 "移动捕获" (需要C++14) auto lambda = [pw = std::make_unique<Widget>()] { ... }; lambda(); // (1) 等效实现: 使用std::bind auto fun_b = std::bind([](const std::unique_ptr<Widget> &pw){ ... // 使用pw. }, std::make_unique<Widget>()); // 绑定给bind的成员是左值, lambda的形参接收的是左值引用. fun_b(); // (2) 等效实现: 使用"仿函数" class MyFunc { public: explicit MyFunc(std::unique_ptr<Widget>&& ptr): pw(std::move(ptr)) {} bool operator()() const { ... // 使用pw } private: std::unique_ptr<Widget> pw; }; auto func = MyFunc(std::make_unique<Widget>())(); // 创建一个函数对象. func(); ``` <br><br><br> # lambda 的返回类型 lambda 表达式省略 "**返回类型**" 时,**将由==编译器自动推断==其返回类型**: - 若 lambda 函数体中**只有==唯一的 return 语句==**,则编译器**根据 `return` 的返回值类型推断**; - 若 lambda 函数体中**还有除 `return` 之外的其他语句**,则**编译器==默认推断其返回类型为 `void`==**。 #### 🚨 返回类型的注意事项 > [!caution] 若 `lambda` 函数体**有除 `return` 外的其他语句,且==具有返回值==**,则 **==必须显式声明返回类型==**,否则编译报错。 > > 原因:**编译器推断的返回类型为 `void`**,而返回类型为 void 的函数不能具有返回值。 > [!caution] `lambda` 表达式用于递归时,**必须显式声明返回类型(即使返回 `void`)** or **必须在递归调用前存在至少一个 `return` 语句**,否则编译错误。 > > > 原因:递归调用时,**还未见到完整函数定义**,因此**无法自动推断返回类型为`void`**,因此报错如下: > > ![[_attachment/02-开发笔记/01-cpp/函数相关/cpp-lambda 表达式.assets/IMG-cpp-lambda 表达式-2FEF4D2CB53D91AE1789E9BFA8728088.png|556]] > > ![[_attachment/02-开发笔记/01-cpp/函数相关/cpp-lambda 表达式.assets/IMG-cpp-lambda 表达式-343E6999BF416B623796DA6FFA0AC6F9.png|564]] > > 解决方法:显式声明返回类型为 `void`,如下: > > ![[_attachment/02-开发笔记/01-cpp/函数相关/cpp-lambda 表达式.assets/IMG-cpp-lambda 表达式-CAC015BB4A7F66E2273EC33EE172D3C0.png|566]] > > <br><br><br> # lambda 使用示例 lambda 用于便捷定义比较函数: ```cpp // 示例一: 使用lambda表达式作为容器的比较函数 auto comp = [](int a, int b) { return a > b; }; priority_queue<int, vector<int>, decltype(comp)) min_heap(comp); // decltype推导函数类型 // 示例二: 使用lambda表达式实现对string类的自定义排序. sort(words.begin(), words.end(), [](const string &a, const string &b) { return a.size() < b.size(); }); // 示例三: 使用lambda表达式实现对idx数组的自定义排序: idx为下标数组, 根据下标索引得到的growTime容器中的值, 由此对下标idx进行排序. sort(idx.begin(), idx.end(), [&]{const int &i, const int& j} { return growTime[i] < growTime[j]; }); ``` <br> ## lambda 表达式实现递归 可通过以下方法实现 lambda 递归: 1. 方式一:使用 `std::function` 为 lambda 提供一个包装,从而在 lambda 内部使用该可调用对象 2. 方式二:通过 **lambda 参数** 来传递 lambda 自身(**推荐**)。 > [!error] lambda 本身是匿名类,故不能通过 "捕获" 的方式在函数体中递归调用 "匿名函数对象"。 > > ```cpp > // Wrong Code > // 下述代码会报错, lambda 内部无法引用自己. > // Variable 'lmb' declared with deduced type 'auto' cannot appear in its own initializer > auto func = [&](int u) -> int { > if (n <= 1) return 1; > return n * func(n - 1); > } > ``` > #### 方式一 **使用 `function` 对象**包装 lambda 表达式,并对其进行**引用捕获**,从而**在 lambda 表达式内部调用 function 对象**实现递归 ```cpp int main() { // 方式一: function<int(int, int)> dfs = [&](int i, int j) -> int { // ... // lambda表达式声明"捕获外部引用"后, 内部直接调用 dfs 这一function对象 return dfs(i+1, j-1); // 内部递归调用 }; dfs(0, n-1); // 外部调用 } ``` #### 方式二(推荐) 通过一个**额外的函数形参**来传递 lambda 对象自身: ```cpp int main() { auto dfs = [](auto& self, int i, int j) -> int { // 写成`auto& self`, 或者`auto&& self`形式都行. // ... // lambda表达式使用一个额外的参数接受其自己, 同时 return self(self, i+1, j-1); // 内部递归调用 } dfs(dfs, 0, n - 1); // 外部调用 } ``` %% > 什么是 Y 组合子?暂时看不太懂,以后再说吧 > > ![image-20231101154857637](_attachment/02-开发笔记/01-cpp/函数相关/cpp-lambda%20表达式.assets/IMG-cpp-lambda%20表达式-A0B1FF4DCD17C59221FB940E4302037C.png) %% <br><br> ## 将 lambda 对象作为函数返回类型 将 lambda 对象作为函数返回只时,该函数的返回类型可通过以下方式声明: 1. 使用 **`auto` 自动推导返回类型**(since C++14) 2. 使用 **`std::function<>` 指明返回类型** 方式一: ```cpp #include <iostream> auto returnLambda() { // since C++14 return [] (int x, int y) { return x*y; }; } int main() { auto lf = returnLambda(); std::cout << lf(6, 7) << std::endl; } ``` 方式二: ```cpp // 使用std::function<> 类模版指明一个一般化类型, 从而可接收lambda函数对象. #include <functional> #include <iostream> std::function<int(int, int)> returnLambda() { return [] (int x, int y) { return x*y; }; } int main() { auto lf = returnLambda(); std::cout << lf(6, 7) << std::endl; } ``` > [!quote] > ![image-20231014140422379|602](_attachment/02-开发笔记/01-cpp/函数相关/cpp-lambda%20表达式.assets/IMG-cpp-lambda%20表达式-DDFB166D0D1F98385E694EA3531DFD84.png) <br><br><br> # 泛型 lambda(C++14) C++14 引入了 "**==泛型 lambda==**"(generic lambdas),可使用 `auto` 作为类型声明符,由编译器对 **形参类型** 自动推导。 同时,也支持在 lambda 中实现 "**完美转发**"。 > [!info] 本质上,泛型 lambda 是对 "**成员函数模版**" 的语法糖 [^5]。 > [!NOTE] 在泛型 lambda 中,要实现完美转发,可将 `decltype(x)` 作为模版参数传递给 `std::forward<>` > > 参见 [^6]: > > ```cpp > auto f = [](auto&& x) { > return func(std::forward<decltype(x)>(x)); > }; > > auto f = [](auto&&... params) { > return func(std::forward<decltype(params)>(params)...); > } > ``` > > ^u95vez <br><br><br> # 全局 lambda(C++17) > [!info] C++17 起,允许在 "**全局命名空间作用域**" 下定义 "==全局 lambda==",但**其不能捕获任何外部全局变量**。 <br><br><br> # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later <br><br> # ♾️参考资料 # Footnotes [^1]: 《Effective Morder C++》Item31 [^2]: 《C++ Primer》(P508) [^3]: 《C++程序设计语言》(P252) [^4]: 《Effective Morder C++》Item32 [^5]: 《C++ Templates》(P65) [^6]: 《Effective Morder C++》Item33