C++:Lambda表达式

1. 匿名函数概念2. Lambda 表达式的表示3. Lambda 表达式各部分3.1 Capture 子句3.1.1 引用捕获3.1.2 值捕获3.1.3 不捕获3.1.4 捕获方式总结3.2 参数列表3.3 可变规范3.4 异常规范3.5 返回类型3.6 函数体4. 嵌套 Lambda 表达式

1. 匿名函数概念

在计算机编程中,匿名函数(英语:anonymous function)是指一类无需定义标识符(函数名)的函数或子程序,普遍存在于多种编程语言中。

在 C++11 和更高的版本中,lambda 表达式通常称为 lambda —— 是一种在调用它或作为参数传递给函数时定义匿名函数对象(闭包)的简便方法。Lambda 通常用于封装传递给算法或异步方法的少量代码。

注:** 本文只讨论 C++11 中的 lambda特性。

2. Lambda 表达式的表示

ISO C++ 标准展示了作为第三个参数传递给 std::sort()函数的简单 lambda:

#include <algorithm>
#include <cmath>

void abssort(float* x, unsigned n) {
    std::sort(x, x + n,
        // Lambda expression begins
        [](float a, float b) {
            return (std::abs(a) < std::abs(b));
        } // end of lambda expression
    );
}

lambda 表达式的完整语法如下:

[captures] (params) specifiers(可选) exception -> ret { body }

下图通过实例展示了 lambda 的组成部分:

01_constituent_part_of_lambda

解释:

  1. captures - 零或更多捕获的逗号分隔列表,可选地以 capture-default 起始。 捕获的详细描述见下方。 若变量满足下列条件,则 lambda 表达式能使用而不捕获它
  • 为非局部变量,或拥有静态或线程局域存储期(该情况下不能捕获该变量),或
  • 为以常量表达式初始化的引用。 若变量满足下列条件,则 lambda 表达式能读取其值而不捕获它
  • 拥有 const 而非 volatile 的整数或枚举类型,并已用常量表达式初始化,或
  • constexpr 且为可平凡复制构造。
  1. params - 参数列表 (也称为lambda 声明符,可选) 若以 auto 为参数类型,则该 lambda 为泛型 lambda 。 (C++14 起)
  2. specifiers - 可变规范(可选)。 可选的指定符序列。允许下列指定符:
  • mutable :允许 body 修改以复制捕获的参数,及调用其非 const 成员函数
  • constexpr :显式指定函数调用运算符为 constexpr 函数。此指定符不存在时,若函数调用运算符恰好满足所有 constexpr 函数要求,则它也会是 constexpr(C++17 起)
  • consteval :指定函数调用运算符为立即函数。不能同时使用 constevalconstexpr 。(C++20 起)
  1. exception - 异常规范(可选)。 为闭包类型的 operator() 提供异常规定或 noexcept 子句。
  2. ret - 返回类型(可选)。若缺失,则由函数的 return 语句所隐含(或若函数不返回任何值则为 void )。
  3. body - lambda 函数体。

Lambda 表达式是纯右值表达式,其类型是独有的无名非联合非聚合类类型,被称为闭包类型,它声明于含有该 lambda 表达式的最小块作用域、类作用域或命名空间作用域。

3. Lambda 表达式各部分

3.1 Capture 子句

Lambda 以 capture 子句开头,指定哪些变量被捕获,以及是通过值还是引用捕获。Lambda 通过在最前面的方括号 [] 来明确指明其内部可以访问的外部变量,这一过程也称为 Lambda 表达式“捕获”了外部变量。

3.1.1 引用捕获

使用引用捕获一个外部变量,只需要在捕获列表变量前面加引用说明符 & 即可,如果捕获列表只有一个 引用说明符但没有变量名称,则表示可以引用访问所有其可以访问到的变量。

示例 3.1.1:

void CaptureByRef()
{
    int total = 20;
    auto func = [&]() {
        cout << "The total num of sutdents is: " << total << endl;

        cout << "There comes a new student. " << endl;
        total++;
    };

    func();

    cout << "Now, the total number of sutdents is: " << total << endl;
}

上述示例在 func 匿名函数中对 total 采用引用访问,并在该函数中对 total 进行加一操作,输出如下:

The total num of sutdents is: 20
There comes a new student.
Now, the total number of sutdents is: 21

3.1.2 值捕获

使用值捕获一个外部变量,只需要在捕获列表变量前面加一个等号 = 即可,如果捕获列表只有一个等号但没有变量名称,则表示可以使用值捕获的方式访问所有其可以访问到的变量。如果我们仅将上述示例的引用访问改为值访问,会怎样?

示例 3.1.2:

void CaptureByValue()
{
    int total = 20;
    auto func = [=]() {
        cout << "The total num of sutdents is: " << total << endl;

        cout << "There comes a new student. " << endl;
        total++;
    };

    func();

    cout << "Now, the total number of sutdents is: " << total << endl;
}

Visual Studio 会提示如下错误,也就是值传递的变量不可以被修改,所以需要将total++; 删掉或移出 lambda 函数。

02_capture_by_value_error

3.1.3 不捕获

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

如果将上述代码中代表值传递的=去掉,就是空 capture 子句:

示例 3.1.3:

void CaptureDefault()
{
    int total = 20;
    auto func = []() {
        cout << "The total num of sutdents is: " << total << endl;
    };

    func();

    cout << "Now, the total number of sutdents is: " << total << endl;
}

此代码会报如下错误封闭函数局部变量不能在lambda体内引用,除非其位于捕获列表中

03_capture_default_error1

假如我们把 total 放入参数列表中,情况如何?

示例 3.1.4:

void CaptureDefault()
{
    int total = 20;
    auto func = [](int total) {
        cout << "The total num of sutdents is: " << total << endl;

        cout << "There comes a new student. " << endl;
        total++;    
        cout << "Now, the total number of sutdents is: " << total << endl;
    };

    func(20);

    cout << "Actually, the total number of sutdents is: " << total << endl;
}

运行结果为:

=========== CaptureDefault test =============
The total num of sutdents is: 20
There comes a new student.
Now, the total number of sutdents is: 21
Actually, the total number of sutdents is: 20

可以看到:虽然参数列表和外部的 total 同名,但是匿名函数并未改变外部 total 的值,这和全局变量与局部变量的差别类似。

3.1.4 捕获方式总结

通过以上示例,基本的捕获方式已经介绍完毕,我们来总结一下:

捕获方式

说明

[]

不捕获任何外部变量

[x1, x2, …]

默认以值捕获的方式捕获指定外部变量 x1, x2, …

[&x1, &x2, …]

以引用捕获的方式捕获指定外部变量x1, x2, …

[this]

以值捕获的方式捕获this指针

[=]

以值捕获的方式捕获所有外部变量

[&]

以引用捕获的方式捕获所有外部变量

[=, &x]

外部变量x以引用捕获方式捕获,其余变量以值捕获的方式捕获

[&, x]

外部变量x以值捕获方式捕获,其余变量以引用捕获的方式捕获

3.2 参数列表

Lambda表达式的参数和普通函数的参数类似,但是在 Lambda 表达式中传递参数还有一些限制,主要有以下几点:

  • 参数列表中不能有默认参数(C++14 起, lambda 能拥有自身的默认参数)
  • 不支持可变参数
  • 所有参数必须有参数名

虽然参数列表中不支持默认参数,但是可以通过 lambda 函数体 后面加一个小括号,在小括号中指定默认值。

void ParamList()
{
    // 指定参数值
    cout << "The total number of sutdents is: " << [](int total) -> int {return total; } (20) << endl;
}

执行结果为:

The total number of sutdents is: 20

3.3 可变规范

通常情况下,lambda 的函数调用运算符是常量的值,但使用的 mutable 关键字就可以改变其值。它不会生成可变的数据成员。 利用可变规范,lambda 表达式的主体可以修改通过值捕获的变量。

示例3.1.2 中在 lambda 函数体内对值捕获的外部变量进行修改会提示错误,我们可以通过加入 mutable 关键字来解决。

示例 3.3.1

void MutableSpecifier()
{
    int total = 20;
    auto func = [=]() mutable {
        cout << "The total number of sutdents is: " << total << endl;

        cout << "There comes a new student. " << endl;
        total++;
        cout << "Now, the total number of sutdents is: " << total << endl;
    };

    func();

    cout << "Actually, the total number of sutdents is: " << total << endl;
}

执行结果:

The total number of sutdents is: 20
There comes a new student.
Now, the total number of sutdents is: 21
Actually, the total number of sutdents is: 20

从输出结果可以看出,加上 mutable 关键字之后,在 lambda 函数体内可以对外部变量进行修改,但是其修改的有效作用域限制于函数体内,在函数体外部该变量并没有修改。

3.4 异常规范

为闭包类型的 operator() 提供异常规定(https://zh.cppreference.com/w/cpp/language/except_spec)或 noexcept 子句(https://zh.cppreference.com/w/cpp/language/noexcept_spec)。

3.5 返回类型

Lambda 表达式的返回类型是自动推导的。 如果不指定返回类型,可以使用 auto 关键字 。 trailing-return-type 类似于普通方法或函数的返回类型部分。 但是,返回类型必须跟在参数列表的后面,你必须在返回类型前面包含 trailing-return-type 关键字 ->

如果 lambda 函数体仅包含一个返回语句或其表达式不返回值,则可以省略 lambda 表达式的返回类型部分。 如果 lambda 函数体包含单个返回语句,编译器将从返回表达式的类型推导返回类型。 否则,编译器会推导返回类型为void。

3.6 函数体

Lambda 函数体可以包含普通方法或函数的主体可以包含的任何内容。 普通函数和 lambda 表达式的主体均可访问以下变量类型:

  • 从封闭范围捕获变量
  • 参数
  • 本地声明变量
  • 当在类中声明类数据成员,this 可以被捕获
  • 具有静态存储持续时间的任何变量(例如,全局变量)

4. 嵌套 Lambda 表达式

Lambda 表达式可以嵌套以实现复杂的应用场景。

示例 4.1:

void NestedLambda()
{
    auto result = [](int x) { 
        return [](int y) { 
            return y * 2; 
        }(x)+3; 
    }(5);

    cout << "result: " << result << endl;
}

该表达式用来计算 5*2+3 的值,其输出结果为:

result: 13

参考链接:

  1. https://zh.cppreference.com/w/cpp/language/lambda
  2. https://docs.microsoft.com/zh-cn/cpp/cpp/lambda-expressions-in-cpp?view=vs-2017
  3. https://www.cnblogs.com/DswCnblog/p/5629165.html

原文发布于微信公众号 - C与Python实战(CPythonPractice)

原文发表时间:2019-01-30

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券