C++—— Lambda表达式

孤街浪徒 提交于 2019-12-08 07:52:28

写在前面

这是对于C++11中新添加的Lambda表达式的学习总结,截至目前遇到过两次这样的题目。可参考:LeetCode-520. Detect CapitalLeetCode-406. Queue Reconstruction by Height LeetCode-155. Min Stack

利用Lambda表达式,可以方便的定义和创建匿名函数。对于C++这门语言来说来说,“Lambda表达式”或“匿名函数”这些概念听起来好像很深奥,但很多高级语言在很早以前就已经提供了Lambda表达式的功能,如C#,Python等。今天,我们就来简单介绍一下C++中Lambda表达式的简单使用。隐约感觉这个好像还和函数式编程有关。所以借此机会学习学习。

参考MSDN中的 C++ 中的 Lambda 表达式C++ 11 Lambda表达式C++11 学习笔记 lambda表达式。有必要说明一下,以上参考都是非常优秀完整的博客,我只是针对其内容进行选择性的整理。只用作个人学习积累。

后续更新

2018-5-14遇到有关Lambda表达式的题目可参考:

  1. LeetCode-56. Merge Intervals

声明Lambda表达式

Lambda表达式完整的声明格式如下:

[capture list] (params list) mutable exception-> return type { function body }

汉语格式参考:C++ Lambda表达式用法

[函数对象参数] (操作符重载函数参数) mutable或exception声明 -> 返回值类型 {函数体}

[函数对象参数] (操作符重载函数参数) mutable或exception声明 -> 返回值类型 {函数体}

具体可参考:MSDN中图
这里写图片描述

对应各项含义如下:

  1. Capture 子句(在 C++ 规范中也称为 lambda 引导。)
  2. 参数列表(可选)。 (也称为 lambda 声明符)
  3. 可变规范(可选)。
  4. 异常规范(可选)。
  5. 尾随返回类型(可选)。
  6. “lambda 体”。

参考博客:C++ 11 Lambda表达式,上述含义也可以解释如下:

  1. capture list:捕获外部变量列表
  2. params list:形参列表
  3. mutable指示符:用来说用是否可以修改捕获的变量
  4. exception:异常设定
  5. return type:返回类型
  6. function body:函数体

由于六个部分中有的可选,所以可以根据实际情况进行适当省略。常见格式有如下:

  1. [capture list] (params list) -> return type {function body}
  2. [capture list] (params list) {function body}
  3. [capture list] {function body}

其中:

  • 格式1声明了const类型的表达式,这种类型的表达式不能修改捕获列表中的值。
  • 格式2省略了返回值类型,但编译器可以根据以下规则推断出Lambda表达式的返回类型: (1):如果function body中存在return语句,则该Lambda表达式的返回类型由return语句的返回类型确定; (2):如果function body中没有return语句,则返回值为void类型。
  • 格式3中省略了参数列表,类似普通函数中的无参函数。

Capture 子句

Lambda表达式可以使用其可见范围内的外部变量,但必须明确声明(明确声明哪些外部变量可以被该Lambda表达式使用)。那么,在哪里指定这些外部变量呢?Lambda表达式通过在最前面的方括号[]来明确指明其内部可以访问的外部变量,这一过程也称过Lambda表达式“捕获”了外部变量。

Lambda 可在其主体中引入新的变量(用 C++14),它还可以访问(或“捕获”)周边范围内的变量。 Lambda 以 Capture 子句(标准语法中的 lambda 引导)开头,它指定要捕获的变量以及是通过值还是引用进行捕获。 有与号 (&) 前缀的变量通过引用访问,没有该前缀的变量通过值访问。

空 capture 子句 [ ] 指示 lambda 表达式的主体不访问封闭范围中的变量。

可以使用默认捕获模式(标准语法中的 capture-default)来指示如何捕获 lambda 中引用的任何外部变量:[&] 表示通过引用捕获引用的所有变量,而 [=] 表示通过值捕获它们。 可以使用默认捕获模式,然后为特定变量显式指定相反的模式。

详细来说,捕获分为值捕获与引用捕获。

1、值捕获
值捕获和参数传递中的值传递类似,被捕获的变量的值在Lambda表达式创建时通过值拷贝的方式传入,因此随后对该变量的修改不会影响影响Lambda表达式中的值。

示例如下:

int main()
{
    int a = 123;
    auto f = [a] { cout << a << endl; }; 
    a = 321;
    f(); // 输出:123
}

需要注意的是:如果以传值方式捕获外部变量,则在Lambda表达式函数体中不能修改该外部变量的值。

2、引用捕获
使用引用捕获一个外部变量,只需要在捕获列表变量前面加上一个引用说明符&。引用捕获的变量使用的实际上就是该引用所绑定的对象。如下:

int main()
{
    int a = 123;
    auto f = [&] { cout << a << endl; };    // 引用捕获
    a = 321;
    f(); // 输出:321
}

从上面例子可以看到,int a的初值是123,f的声明在123之后,后来a的值更改为321。这是输出f(),发现a的值发生了改变。这可以与按值捕获相对比,不难发现,按值捕获,a的值改变不会影响f()中的结果,但是按照引用捕获,会影响。

3.隐式捕获

以上两种例子都需要在捕获列表中表面需要捕获的外部变量,但是我们也可以让编译器根据lambda体中需要的代码来推断需要捕获哪些变量。这就是隐式捕获。

隐式捕获有两种方式,分别是[=]和[&]。[=]表示以值捕获的方式捕获外部变量,[&]表示以引用捕获的方式捕获外部变量。

隐式引用捕获示例:

1.使用[=]

int main()
{
    int a = 123;
    auto f = [=] { cout << a << endl; };    // 值捕获
    f(); // 输出:123
}

2.使用[&]

int main()
{
    int a = 123;
    auto f = [&] { cout << a << endl; };    // 引用捕获
    a = 321;
    f(); // 输出:321
}

4.综合使用

综合起来,例如,如果 lambda 体通过引用访问外部变量 total 并通过值访问外部变量 factor,则以下 capture 子句等效:

[&total, factor]  
[factor, &total]  
[&, factor]  
[factor, &]  
[=, &total]  
[&total, =]  

解释可参考:
捕获列表:lambda表达式的捕获列表精细控制了lambda表达式能够访问的外部变量,以及如何访问这些变量。

  1. []不捕获任何变量。
  2. [&]捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。
  3. [=]捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)。
  4. [=,&foo]按值捕获外部作用域中所有变量,并按引用捕获foo变量。
  5. [bar]按值捕获bar变量,同时不捕获其他变量。
  6. [this]捕按值获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样的访问权限。如果已经使用了&或者=,就默认添加此选项。捕获this的目的是可以在lamda中使用当前类的成员函数和成员变量。

如果 capture 子句包含 capture-default &,则该 capture 子句的 identifier 中没有任何 capture 可采用 & identifier 形式。 同样,如果 capture 子句包含 capture-default =,则该 capture 子句的 capture 不能采用 = identifier 形式。 identifier 或 this 在 capture 子句中出现的次数不能超过一次。 以下代码片段给出了一些示例。

struct S { void f(int i); };  
  
void S::f(int i) {  
    [&, i]{};    // OK  
    [&, &i]{};   // ERROR: i preceded by & when & is the default  
    [=, this]{}; // ERROR: this when = is the default  
    [i, i]{};    // ERROR: i repeated  
}  

params list参数列表

除了捕获变量,lambda 还可接受输入参数。 参数列表(在标准语法中称为 lambda 声明符)是可选的,它在大多数方面类似于函数的参数列表。例:

int y = [] (int first, int second)  
{  
    return first + second;  
};  

在 C++14 中,如果参数类型是泛型,则可以使用 auto 关键字作为类型说明符。 这将告知编译器将函数调用运算符创建为模板。 参数列表中的每个 auto 实例等效于一个不同的类型参数。

auto y = [] (auto first, auto second)  
{  
    return first + second;  
};  

虽然Lambda表达式的参数和普通函数的参数类似,但是还是要注意有些许区别,主要有以下几点:

  1. 参数列表中不能有默认参数
  2. 不支持可变参数
  3. 所有参数必须有参数名

mutable指示符

在前面有说过,在Lambda表达式中,如果以传值方式,或者说值捕获的方式捕获外部变量,则函数体中不能修改该外部变量,这一点与子函数创立参数类似。否则会引发编译错误。那么有没有办法可以修改值捕获的外部变量呢?这是就需要使用mutable关键字,该关键字用以说明表达式体内的代码可以修改值捕获的变量,示例:

int main()
{
    int a = 123;
    auto f = [a]()mutable { cout << ++a; }; // 不会报错
    cout << a << endl; // 输出:123
    f(); // 输出:124
}

需要注意:被mutable修饰的lambda表达式就算没有参数也要写明参数列表。

异常规范

可以使用 throw() 异常规范来指示 lambda 表达式不会引发任何异常。 与普通函数一样,如果 lambda 表达式声明 C4297 异常规范且 lambda 体引发异常,Visual C++ 编译器将生成警告 throw(),如下所示:

// throw_lambda_expression.cpp  
// compile with: /W4 /EHsc   
int main() // C4297 expected  
{  
   []() throw() { throw 5; }();  
}  

返回类型

编译器将自动推导 lambda 表达式的返回类型。 无需使用 auto 关键字,除非指定尾随返回类型。 trailing-return-type 类似于普通方法或函数的返回类型部分。 但是,返回类型必须跟在参数列表的后面,你必须在返回类型前面包含 trailing-return-type 关键字 ->。

如果 lambda 体仅包含一个返回语句或其表达式不返回值,则可以省略 lambda 表达式的返回类型部分。 如果 lambda 体包含单个返回语句,编译器将从返回表达式的类型推导返回类型。 否则,编译器会将返回类型推导为 void。 下面的代码示例片段说明了这一原则。

auto x1 = [](int i){ return i; }; // OK: return type is int  
auto x2 = []{ return{ 1, 2 }; };  // ERROR: return type is void, deducing   
                                  // return type from braced-init-list is not valid  

Lambda 体

lambda 表达式的 lambda 体(标准语法中的 compound-statement)可包含普通方法或函数的主体可包含的任何内容。 普通函数和 lambda 表达式的主体均可访问以下变量类型:

从封闭范围捕获变量,如前所述。

  1. 参数
  2. 本地声明变量
  3. 类数据成员(在类内部声明并且捕获 this 时)
  4. 具有静态存储持续时间的任何变量(例如,全局变量)

以下示例包含通过值显式捕获变量 n 并通过引用隐式捕获变量 m 的 lambda 表达式:

// captures_lambda_expression.cpp  
// compile with: /W4 /EHsc   
#include <iostream>  
using namespace std;  
  
int main()  
{  
   int m = 0;  
   int n = 0;  
   [&, n] (int a) mutable { m = ++n + a; }(4);  //mutable只是一个标识符,后面的就是Lambda体。
   cout << m << endl << n << endl;  
}  
//Output:
5
0

由于变量 n 是通过值捕获的,(变量m是通过引用捕获的),因此在调用 lambda 表达式后,变量的值仍保持 0 不变。 4是通过参数列表里的a传进。mutable 规范允许在 lambda 中修改 n。

其他需要注意的

没有捕获变量的lambda表达式可以直接转换为函数指针,而捕获变量的lambda表达式则不能转换为函数指针。原因如下:

lambda表达式可以说是就地定义仿函数闭包的“语法糖”。它的捕获列表捕获住的任何外部变量,最终均会变为闭包类型的成员变量。按照C++标准,lambda表达式的operator()默认是const的,一个const成员函数是无法修改成员变量的值的。而mutable的作用,就在于取消operator()的const。

typedef void(*Ptr)(int*);  
  
Ptr p = [](int* p) { delete p; };              //OK  
Ptr p1 = [&] (int* p) { delete p; };         //error  
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!