浅谈C++中的lambda表达式、function对象以及闭包性质

在程序设计语言中不止把基本数据或者复合数据当作对象,一个函数同样也是一个对象,本文将会对C++中的匿名函数对象lambdafunction做简单的讨论,最后将会利用这些讨论程序语言中的一个重要性质—闭包

1.lambda表达式和function对象

1.1.lambda表达式

1.1.1.定义

  • 一个lambda表达式表示一个可调用的代码单元,即一个匿名的函数。它具有一个返回类型、一个参数列表和一个函数体,与函数不同的是它没有名字并且可以定义在函数内部

    形式为:[capture list] (parameter list) -> return type {function body}

    其中:

    • capture list(捕获列表)是表达式所在函数中定义的局部变量的列表。
    • parameter listreturn typefunction body分别就是和普通函数一样的形参列表,返回值类型、函数体。需要注意的是lambda使用的是尾置返回(跟在形参列表之后并以一个->符号开头)。
  • 我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体

    1
    2
    auto f = [] {return 1;}; //f是一个返回1的函数
    cout << f() << endl; //打印出1,因为我们调用了 f 返回的函数
  • 如果忽略返回类型,编译器会根据函数体中的代码推断出返回类型。有下面的2点要注意:

    • 如果函数体只是一个return语句,则返回类型从return表达式的类型推断而来。
    • 如果函数体里还有任何单一return语句之外的内容,且未指定返回类型,则返回void

1.1.2.传递参数

  • 与普通函数类似,调用一个lambda时给定的实参被用来初始化lambda的形参。

    下面的代码使用库函数sortvector<int>类型对象nums进行排序,当需要比较两个元素时会调用lambda表达式。

    1
    2
    sort(nums.begin(), nums.end(),
    [] (const int &a, const int &b) { return a < b;});

1.1.3.捕获列表

  • 当我们需要在lambda表达式中使用所在函数体的局部变量时,通过将局部变量包含进捕获列表中来指出将会使用这些变量

    一个重要原则是:尽量保持lambda的变量捕获简单化。因此简单介绍一下常用的值捕获引用捕获两种方式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void fun1(){
    int v1 = 1; //局部变量
    auto f = [v1] { return v1;}; //将v1拷贝给可调用对象 f
    v1 = 0;
    cout << f() << endl; //结果为 1,f 保存了创建它时 v1 的拷贝
    }

    void fun2(){
    int v1 = 1; //局部变量
    auto f = [&v1] { return v1;}; //f 包含v1 的引用
    v1 = 0;
    cout << f() << endl; //结果为 0,f 保存是v1 的引用
    }

1.1.4.返回类型

  • 在返回类型被忽略时,如果函数体只是一个return语句,则返回类型从return表达式的类型推断而来。

    1
    2
    3
    //判断 x 是不是正数,内部只有一条return语句,是对的
    auto isPositive = [] (int x) { return i > 0 ? true : false;};
    bool a = isPositive(1);
  • 在返回类型被忽略时,如果函数体里还有任何单一return语句之外的内容,且未指定返回类型,则返回void

    1
    2
    3
    //absolute返回类型为void
    auto absolute = [] (int x) { if(x > 0) return x; else return -x;};
    cout << absolute(-2) << endl;
  • 当我们为一个lambda定义返回类型时必须使用尾置返回类型

    1
    2
    3
    //absolute返回类型为int
    auto absolute = [] (int x) -> int { if(x > 0) return x; else return -x;};
    cout << absolute(-2) << endl;

1.2.function对象

1.2.1.不同类型的可调用对象可能具有相同的调用形式

  • C++中有这几种可调用的对象:函数函数指针lambda表达式重载了函数调用运算符的类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //普通函数
    int add(int i, int j) { return i + j;};

    //lambda表达式
    auto mod = [] (int i, int j) { return i % j};

    //函数对象类
    struct div{
    //重载调用运算符
    int operator() (int denom, int divisor){
    return denom / divisor;
    }
    }

    上面的函数虽然对参数进行了不同的操作,但是它们的形参和返回值类型都是相同的,可以说它们是共享了同一种调用形式

    int (int, int)

    有没有一种方式让我们描述这种调用方式呢?

1.2.2.标准库function类型

  • 我们使用一个叫做function的新的标准库类型来描述一个共享的调用方式,它定义在了functional头文件中。

    function是一个模板,和其他模板一样,我们在一对尖括号内指定类型,对于上面的共享形式,可以这样声明:

    function<int (int, int)>

    它表示可以接受两个int,返回一个int的可调用对象。

    下面我们利用它来定义几个简单的算术函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function<int (int, int)> f1 = add; //函数指针

    function<int (int, int)> f2 = mod; //命名的lambda表达式

    function<int (int, int)> f3 = div(); //函数类的对象

    function<int (int, int)> f4 = [] (int i, int j) { return i * j;}; //匿名lambda表达式

    function<int (int, int)> f5 = std::minus<int> (); //标准库函数对象

1.3.一个综合实验

这里我将使用function对象声明函数的形参和返回值,以及利用lambda表达式进行传值。

  • 完整的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    #include<iostream>
    #include<functional>
    #include<vector>
    using namespace std;

    /**
    * 实现了一个过滤器
    * 输入:int 类型的 vector 对象 nums,一个一元谓词函数 f
    * 输出:nums 中满足 f 条件的所有元素
    */
    vector<int> filter(vector<int>& nums, function<bool (int)> f){
    vector<int> res;
    for(int val : nums){
    if(f(val))
    res.push_back(val);
    }
    return res;
    }

    /**
    * 实现一个平均阻尼器
    * 输入:一个函数对象 f
    * 输出:一个单参数的函数,这个函数返回 f 在 x 处的平均阻尼
    */
    function<double (double)> averageDamp(function<double (double)> f){
    auto val = [&f] (const double &x) { return (f(x) + x) / 2.0 ;};
    return val;
    }

    int main(){
    //判断是不是偶数
    auto isEven = [] (const int &val) -> bool { if(val % 2 == 0) return true; else return false;};
    vector<int> nums = {1, 3, 2, 4, 6, 9, 10, 7, 5, 11};
    vector<int> res = filter(nums, isEven);
    for(int val : res)
    cout << val << " ";
    cout << endl << "Enter a number x: ";
    double input;
    cin >> input;
    cout << " (x + x^2) / 2 = " << averageDamp([](double x){return x*x;})(input) << endl;
    return 0;
    }
  • 运行结果:

    closure

2.C++中的闭包

  • 在计算机科学中闭包是一个非常重要的性质,下面是维基百科中对于它的一点介绍,想了解更多的话请点击这里>)。

    在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。

    所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

简单的说:闭包就是带上了状态的函数,就是把一个函数和一个变量之间创建了联系,在给定函数被多次调用的过程中,这些变量能够保持其持久性。闭包是将函数内部和函数外部连接起来的桥梁

C++中的lambda表达式就可以方便地实现一个闭包。因为它最简单的生成了一个可捕获局部变量的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<iostream>
#include<functional>
using namespace std;

function<void (void)> drinkMilk(int milk){
//返回一个判断牛奶数量并喝牛奶的过程
return [&milk] {
if( milk > 1)
cout << "I was drunk by a bottle of milk, There are still " << --milk << " bottles of milk." << endl;
else if(milk == 1){
cout << "I've just finished the last bottle of milk." << endl;
--milk;
}
else
cout << "Sorry,All the milk has been drunk." << endl;
};
}

int main(){
int milk = 5; //初始化牛奶是5瓶
cout << "Now I have 5 bottles of milk." << endl << endl;

//创建一个喝牛奶的过程
auto perry = drinkMilk(milk);

//开始模拟
for(int i = 0; i < 6; ++i){
perry();
cout << endl;
}
return 0;
}

运行结果:

closure2

  • 虽然闭包给我们的编程带来了极大的便利性,但需要注意的是闭包建立的这种联系破环了局部作用域,在一些情况下是危险的,并且消耗了一定量的内存空间。要小心使用!!!
------------- 本文结束 感谢您的阅读 -------------

本文标题:浅谈C++中的lambda表达式、function对象以及闭包性质

文章作者:Perry

发布时间:2019年04月09日 - 00:07

最后更新:2019年09月19日 - 13:46

原始链接:https://perry96.com/archives/b264b81e.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%