lambda表达式详解


前言

一、 简单来讲,什么是lambda表达式?

lambda表达式是匿名函数,就是说不用定义函数名,函数实现可以直接嵌入在业务逻辑代码中。

二、 简单说一下其优点:

其优点是提高了代码的可读性,对于一些无需重用的方法特别适合。例如在容器的迭代中实现特定的查询逻辑。

三、 写一下他的语法和示例

[captures] (params) specifiers exception -> ret {body}
1、 [captures] —— 捕获列表,它用于捕获当前函数作用域的零个或多个变量,变量之间用逗号分隔。
2、 (params) —— 可选参数列表,其语法与普通函数参数列表一致。如果不需要参数,则可以忽略此项。
3、 specifiers —— 可选限定符,可选值为mutable。其意义是可以在函数体内修改按值捕获的变量。
4、 exception —— 可选异常说明符,可以使用noexcept来指明lambda是否会抛出异常。
5、 ->ret —— 可选返回值类型,lambda使用返回类型后置的语法来表示返回类型。如果没有返回值 ,则可忽略此部分。如果不指定返回类型,则编译器会根据代码实现为函数推导一个返回类型。
6、 {body} —— 表达式的函数体,此部分与实现普通函数体一致。
示例程序:

#include <iostream>
int main() {
    int x = 10;
    auto foo = [x](int y)->int { return x + y; };
    std::cout << foo(7) << std::endl;
}

增加mutable之后,对x变量进行修改,看看会发生什么?

int main() {
    int x = 10;
    auto foo = [x](int y) mutable ->int { x++; return x + y; };
    std::cout << foo(7) << std::endl;
    std::cout << foo(7) << std::endl;
    std::cout << foo(7) << std::endl;
}

四、捕获列表

作用域(必须知道)
关于lambda表达式的作用域,有四个重点:
第一,捕获列表的变量有两个作用域,一是lambda定义的函数作用域,二是表达式函数体所在代码的作用域;
第二,在表达式函数体内,默认情况下,被捕获变量是只读属性。如需修改,则要添加mutable标识;
第三,在表达式函数体内,修改被捕获变量的值,不影响原始变量的值;
第四,被捕获变量必须是非静态局部变量,在函数体内可以使用静态或全局变量。

好,为了加强印象,我们先从面试挖坑场景开始。请先看第一坑:

int main() {
    int x = 10;
    auto foo = [x](int y) ->int { x++; return x + y; };
    std::cout << foo(8) << std::endl;
}

问:上述代码能通过编译吗?如果不能,是为什么?
答:不能。因为根据规则一和二,在表达式函数体内,x变量是只读,不能被修改。

好,上面的只是开胃菜,诡异的第二坑来了:

int main() {
    int x = 10;
    auto foo = [x](int y) mutable ->int {
        x++;
        std::cout << x << std::endl;
        return x + y;
    };
    foo(8);
    foo(8);
    std::cout << x << std::endl;
}

问:在上述代码执行完后,请说出所有std::cout语句输出什么内容?
答:根据规则三,分别输出11,12,10。
所以大家可以看到,前两次输出是在表达式体内对x值进行修改,x的状态是保留的了。但在函数体外,x变量的值仍然保持不变。千万要记住这个规则,这就是存在两个作用域所得到的结果。后面我们会讲解其实现原理,这样就可以方便记忆这条规则了。

再来第三坑:

int y = 100;
int main() {
    static int x = 10;
    auto foo = [x, y](int z) ->int { return x + y + z; };
    std::cout << foo(8) << std::endl;
}

问:上述代码有什么问题,应该如何调整?
答:根据作用域规则四,x与y都不能作为lambda表达式的捕获列表变量。在表达式函数体内可以直接使用静态或者全局变量,所以只要修改为auto foo = [](int z) ->int { return x + y + z; };即可。
虽然上面这段代码放在gcc编译器下不会报错,只是出警告:capture of variable ‘x’ with non-automatic storage duration。但编译器实际上是作了一次选择,即认为你的意图是作为全局或静态变量来使用。可如果你只是手误或者忘了要调整代码,那就会出现预料之外的运行结果。

五、捕获引用

上一节我们所讲的,都是捕获值的用法。那么想将变量作为引用传递进lambda表达式,是否可以呢?
答案是肯定的,示例如下:

int main() {
    int x = 10;
    int y = 11;
    auto foo = [&x, &y](int z) mutable ->int {
        x++;
        y++;
        return x + y + z;
    };
    std::cout << foo(8) << std::endl;
    std::cout << x << std::endl;
    std::cout << y << std::endl;
}

输出结果:31、 11、 12
可以看到,捕获引用就是在变量前加上&符号即可。切记不要与取地址相混淆。被引用变量在函数体内修改,也会影响函数体外同名变量的值,这一点与我们以往的认知相同。

六、特殊用法

捕获列表支持三种特殊情况,下面分别说明:
[this] —— 捕获this指针,可以在类内的lambda表达式函数体内使用,以访问成员变量和方法;
[=] —— 捕获当前作用域内全部变量的值,包括this。当变量较多时,可使用此符号;
[&] —— 捕获当前作用域内所有变量的引用,包括this。

先看[this]的示例:

class CaptureOne {
    public:
        void test() {
            auto foo = [this] { std::cout << x << std::endl; };
            foo();
        }
    private:
        int x = 100;
};
int main() {
    CaptureOne one;
    one.test();
}

再看[=]的示例:

int main() {
    int x = 10;
    int y = 11;
    int z = 12;
    auto foo = [=]() mutable ->int {
        x++;
        y++;
        return x + y + z;
    };
    std::cout << foo() << std::endl;
    std::cout << x << std::endl;
    std::cout << y << std::endl;
}

可以看到,这的确是方便了代码书写。如果表达式函数体内的变量增加或减少,都不必再费心思去调整捕获列表了。
另外,不知道大家还注意到一个细节没有,就是当参数列表为空时,如果存在mutable标识,则()是不能省略的。
最后,[&]还是烦请各位自己去实践并体验。

七、实现原理

C++多年老手,在上手lambda表达式的时候,应该会马上在脑海里跟函数对象关联起来。我们先从定义一个函数类开始说起。所谓函数类,就是定义一个类,然后重载operator ()。这样可以将类对象作为函数一样来调用。
示例如下:

class FuncOne {
    public:
        FuncOne(int x, int y) : valx(x), valy(y){}
        int operator () () {
            return valx + valy;
        }
    private:
        int valx;
        int valy;
};

int main() {
    FuncOne one(10, 20);
    std::cout << one() << std::endl;
}

上述代码就是函数类的定义与函数对象的使用方法。而C++11标准中的lambda表达式,其实就是在编译器在编译期生成一个闭包类(可以理解为类似于FuncOne的类),然后在运行时由这个闭包类生成一个对象,称之为闭包。
而闭包就是一个匿名的函数对象,它包含了定义时作用域的上下文。这样我们就会发现lambda表达式,其实就是C++11给我们提供的一个语法糖。
现在再回头去理解捕获列表的四条原则,是否有恍然大悟之感?理解了原理,对于原则就不用死记硬背了,祝各位面试好运!

八、应用

当然,我们不能满足于只过面试关,还应当在工作中用好它才行。做C++开发的,用好STL是基本功,而lambda表达式与STL相结合,可以简化代码,发挥出强大的威力来。
先看一个需求:我们要在一组vector列表中,打印可以整除4的数。
示例代码:

#include        <iostream>
#include        <vector>
#include        <algorithm>
int main() {
    std::vector<int> num_list = {1, 3, 9, 10, 12, 17, 18};
    std::cout << *std::find_if(num_list.cbegin(), num_list.cend(),
            [](int i) { return (i % 4) == 0;}) << std::endl;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值