C++进阶(6)——lambda表达式

编程达人挑战赛·第1期 10w+人浏览 378人参与

目录

基本概念

lambda表达式的语法

捕捉列表

lambda表达式的具体应用

lambda表达式的实现原理


基本概念

我们的lambda表达式本质上就是一个匿名函数对象,和我们的普通函数不同的是他是可以定义在函数的内部的,其实不光光我们的C++支持了lambda表达式,我们的很多的高级语言(Java, Python, C#)都是支持的,而且这一语法是使用频率比较高的语法之一,它可以提高我们的代码可读性,下面我们就来看看吧。

lambda表达式的语法

[capture-list] (parameters)-> return type {
    function body
}

相关说明:

[capture-list]:捕捉列表,这个列表出现在我们的lambda函数的开始位置,编译器根据[ ]来判断接下来的代码是不是lambda函数,捕捉列表能够捕捉我们上下文中的变量以供lambda函数使用,捕捉列表可以传值或是传引用捕捉,即使列表为空也是不可以省略的(重点)。

(parameters):参数列表,和我们的普通参数列表的功能相类似,如果不要传递参数,就可以同我们的( )一起省略掉。

->return type:返回值类型,用来追踪返回类型形式声明函数的返回值类型,没有返回值的时候这个部分可以省略掉,一般的返回值类型明确的情况下,也是可以省略的,可以由编译器对返回值类型进行自我推导。

{function body}:函数体,函数体的实现和我们的普通函数完全类似,除了可以使用其参数外,我们还可以使用我们上面捕获到的变量,函数体不可以省略。

根据我们上面的介绍,我们可以写出我们最简单的lambda表达式了,虽然这个代码什么也没干:

#include <functional>
#include <iostream>
using namespace std;
int main() {
    []{};
    return 0;
}

接下来我们就可以简单地使用一下我们的lambda表达式了:

示例代码:

#include <iostream>
using namespace std;
int main() {
    auto add = [](int x, int y)->int {
        return x + y;
    };
    cout << add(1, 2) << endl;
    auto func = [] {
        cout << "hello world!" << endl;
    };
    func();
    int a = 1, b = 2;
    auto swap = [](int& x, int& y) {
        int temp = x;
        x = y;
        y = temp;
    };
    swap(a, b);
    cout << a << ":" << b << endl;
    return 0;
}

测试效果如图:

捕捉列表

我们的lambda表达式默认只是泗洪lambda函数体和参数中的变量,想使用我们的外层变量就要进行捕捉操作了。我们这里有常见的三种捕捉方式:

第一种方式:

在捕捉列表中显示的传值或是传引用捕捉,多个变量就用逗号分隔。

[x, y, &z]:x和y是值捕捉,z是引用捕捉。

第二种方式:

我们在捕捉列表中可以显示那就可以隐式捕捉,有两种:

[=]:捕捉值

[&]:捕捉引用

这样我们的lambda表达式用了什么变量,编译器就会自动捕捉那些变量了。

第三种方式:

我们还可以将上面的两种方式混合起来。

[=, &x]:表示的是其他变量使用隐式值捕捉,但是x引用捕捉。

[&, x, y]:表示的是其他变量使用隐式引用捕捉,但是我们的x和y使用值捕捉。

这里要注意的是,我们的第一个元素必须是&或是=,是&的时候后面就必须是值捕捉,是=的时候后面就必须是引用捕捉。

敲黑板:

1、lambda表达式如果是在函数的局部域中,它可以捕捉lambda位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量本身也不需要捕捉,lambda表达式中可以直接使用,这也就意味着lambda表达式如果是定义在全局的位置,那么我们的捕捉列表就必须要是空的。

2、默认情况下,我们这里的lambda捕捉列表是被const修饰的,也就是说我们传值捕捉来的对象是不可以被修改的,我们可以使用mutable加在参数列表的后面来取消其常量性,也就是说我们这个时候的传值捕捉对象就是可以修改的了,但是修改的还是形参对象,并不会影响实参。使用这个修饰符后,参数列表就不可以省略了(即使参数为空)。

代码示例:

#include <iostream>
using namespace std;
int x = 0;
// 这里是全局,没有什么可以捕捉的变量
auto func1 = []() {
    x++;
};
int main() {
    int a = 0, b = 1, c = 2, d = 3;
    auto func2 = [a, &b] {
        // 值的捕捉不可以修改,但是引用捕捉的变量是可以修改的
        // a++; // 报错
        b++;
        int ret = a + b;
        return ret;
    };
    cout << func2() << endl; 
    cout << b << endl;
    cout << "*********************" << endl;

    // 隐式值捕捉
    auto func3 = [=] {
        int ret = a + b + c;
        return ret;
    };
    cout << func3() << endl;
    cout << "*********************" << endl;

    // 隐式引用捕捉
    cout << a << " " << b << " " << c << " " << d << endl;
    auto func4 = [&] {
        a++;
        c++;
        d++;
    };
    func4();
    cout << a << " " << b << " " << c << " " << d << endl;
    cout << "*********************" << endl;

    // 混合捕捉1
    cout << a << " " << b << " " << c << " " << d << endl;
    auto func5 = [&, a, b] {
        // a++; // 报错
        // b++; // 报错
        c++;
        d++;
    };
    cout << a << " " << b << " " << c << " " << d << endl;
    cout << "*********************" << endl;

    // 混合捕捉2
    cout << a << " " << b << " " << c << " " << d << endl;
    auto func6 = [=, &a, &b] {
        a++;
        b++;
        // c++; // 报错
        // d++; // 报错
    };
    func6();
    cout << a << " " << b << " " << c << " " << d << endl;
    cout << "*********************" << endl;

    // 局部静态变量和全局变量不能捕捉,也不需要捕捉
    static int y = 2;
    auto func7 = [] {
        int ret = x + y;
        return ret;
    };
    cout << func7() << endl;
    cout << "*********************" << endl;

    // 传值捕捉是拷贝默认是被const修饰的
    // 使用mutable可以消去const属性,但是我们的改变并不会影响外面的值
    cout << a << " " << b << " " << c << " " << d << endl;
    auto func8 = [=]()mutable {
        a++;
        b++;
        c++;
        d++;
        return a + b + c + d;
    };
    cout << func8() << endl;
    cout << a << " " << b << " " << c << " " << d << endl;
    cout << "*********************" << endl;
    return 0;
}

测试效果如图:

lambda表达式的具体应用

我们在学习使用lambda表达式之前,我们使用的可调用对象就是函数指针和仿函数对象了,这两种方式都比较的麻烦,于是我们的就可以使用lambda表达式,不仅方便还具有很高的代码可读性。

我们在实现一些比较器的时候,使用lambda表达式就会比较方便,我们这里还是来举个栗子:

代码如下:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Goods {
    string _name; // 商品名
    double _price; // 商品价格
    int _evaluate; // 商品评价
    // 构造函数
    Goods(const char* name, double price, int evaluate)
    :_name(name)
    ,_price(price)
    ,_evaluate(evaluate){}
};

struct ComparePriceLess {
    bool operator()(const Goods& g1, const Goods& g2) {
        return g1._price < g2._price;
    }
};

struct ComparePriceGreater {
    bool operator()(const Goods& g1, const Goods& g2) {
        return g1._price > g2._price;
    }
};

int main() {
    vector<Goods> v = {{"苹果", 1.2, 5}, {"香蕉", 3.3, 3}, {"橘子", 2.3, 4}, {"栗子", 1.1, 5}};
    // 使用我们的仿函数
    // 按价格升序
    sort(v.begin(), v.end(), ComparePriceLess());
    // 按价格降序
    sort(v.begin(), v.end(), ComparePriceLess());
    // 使用我们的lambda表达式
    // 按价格升序
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
        return g1._price < g2._price;
    });
    // 按价格降序
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
        return g1._price > g2._price;
    });
    // 实现评价从低到高
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
        return g1._evaluate < g2._evaluate;
    });
    // 实现评价从高到低
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
        return g1._evaluate > g2._evaluate;
    });
    return 0;
}

lambda表达式的实现原理

其实聪明的友友可能已经猜到了lambda表达式的实现原理,它的实现原理其实和我们之间讲C++11中的for一样,编译之后从汇编的角度看,就没有什么范围for,范围for的底层就是调用迭代器,我们的lambda也是一样的,底层其实就是生成了对应的仿函数的类。

重点说明一下:

我们这里的底层仿函数是编译的时候按照一定的规则自动生成的,保证了不同的lambda表达式的类名是不一样的,lambda的参数/返回值类型/函数体其实就是我们的仿函数operator()的参数/返回值类型/函数体,lambda表达式的捕捉列表的本质就是生成仿函数的成员变量,也就是说我们的捕捉列表的变量都是lambda类构造函数的实参,隐式捕捉就要看传入了什么变量了。

我们这里还是来写个代码通过反汇编来验证一下:

代码示例:

#include <iostream>
using namespace std;
class Rate {
public:
    Rate(double rate) : _rate(rate) {}
    double operator()(double money, int year) {
        return money * _rate * year;
    }
private:
    double _rate;
};

int main() {
    double rate = 0.21;
    // 使用lambda表达式
    auto r1 = [rate](double money, int year) {
        return money * rate * year;
        };
    r1(10000, 3);
    // 使用函数对象
    Rate r2(rate);
    r2(10000, 3);
    return 0;
}

我们将这个代码转到反汇编来看:

我们通过下面这个图可以看到,我们一开始调用了Rate类的构造函数,然后我们在使用对象r2的时候调用了Rate类的()运算符重载函数。

同样的,如果我们将lambda表达式的代码也转到反汇编,我们可以看到这里的反汇编和上面的是很类似的,首先是调用了<lambda_uuid>类的构造函数,然后在使用对象r1的时候,就会调用<lambda_uuid>类的( )运算符重载函数。

我们这里为了严谨,将我们的可以验证一下我们不同的lambda表达式的类型名是不是一样的:

示例代码:

#include <iostream>
using namespace std;
class Rate {
    public:
        Rate(double rate) : _rate(rate) {}
        double operator()(double money, int year) {
            return money * _rate * year;
        }
    private:
        double _rate;
};

int main() {
    double rate = 0.12;
    auto r1 = [rate](double money, int year) {
        return money * rate * year; 
    };
    auto r2 = [rate](double money, int year) {
        return money * rate * year; 
    };
    cout << typeid(r1).name() << endl;
    cout << typeid(r2).name() << endl;
    return 0;
}

测试效果:

我们这里的结果是缩写的结果,但是还是显示了它们的不同之处。

敲黑板:

我们这里的类名处理成了<lambda_uuid>,这里的uuid即使我们的通用唯一识别码(Universally Unique Identifier),这个码在编程中经常被使用,这个码可以保证我们的类名唯一性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

西阳未落

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值