c++中的lambda表达式

简介 & 用法

lambda表达式是c++11引入的一个重要特性,基本语法如下

[捕获列表](形参列表) -> 返回类型 {
    // 函数体
}

其中捕获列表和形参列表可以为空,返回值类型大部分情况下可以忽略不写。
lambda表达式的结构整体上和普通函数一样,特殊在比普通函数多一个捕获列表。捕获列表的作用是获取作用域以外的局部变量给函数体使用。捕获器有两种形式:

  • 按值捕获:语法[localValue],原理和形参列表的按值传递类似。
  • 按引用捕获:语法[&localValue],原理和形参列表的按引用传递类似。

举个例子

int main {
    int base = 100;
    int count = 0;

    auto trans = [base, &count](int value) {
        ++count;
        return base + value;
    };

    int res = trans(5);
    printf("base=%d, count=%d, res=%d\n", base, count, res);
}

示例代码中的lambda表达式trans按值捕获局部变量base、按引用捕获局部变量count,因此lambda表达式内部修改base不会影响外部base的值,而修改count则会影响外部count的值。

lambda表达式的原理

本质:lambda本质上是函数对象(函数对象的介绍见本文最后一节 扩展知识)。
为了弄清楚lambda表达式的原理,我们需要研究研究示例代码的汇编码

main::{lambda(int)#1}::operator()(int) const:
        push    rbp
        ...
        ret
main:
        push    rbp
        ...
        call    main::{lambda(int)#1}::operator()(int) const
        ...
        ret

函数调用语句int res = trans(5);对应的汇编码为call指令,调用的函数符号main::{lambda(int)#1}::operator()(int)代表的是匿名类main::{lambda(int)#1}的函数调用操作符operator()
函数操作符operator()?这不是函数对象嘛!为了一探究竟,我们用函数对象重写示例代码对比两者的汇编码看看

int main() {
    int base = 100;
    int count = 0;

    // auto trans = [base, &count](int value) {
    //     ++count;
    //     return base + value;
    // };

    struct TransLambda {
        TransLambda(int& count) : _count(count) { }

        int operator()(int value) {
            ++_count;
            return _base + value;
        }

        int& _count;
        int _base;
    };
    TransLambda trans(count);

    int res = trans(5);
}

这段代码的汇编码如下

main::TransLambda::TransLambda(int&) [base object constructor]:
        push    rbp
        ...
        ret
main::TransLambda::operator()(int):
        push    rbp
        ...
        ret
main:
        push    rbp
        ...
        call    main::TransLambda::TransLambda(int&) [complete object constructor]
        ...
        call    main::TransLambda::operator()(int)
        ...
        leave
        ret

对比lambda表达式版本和函数对象两个版本的汇编码,发现两段汇编码的内容几乎一样。破案了,lambda表达式就是函数对象,也可以认为lambda是函数对象的语法糖。
为了进一步理解,我们不妨大胆猜测一下编译器拿到lambda函数后做了几件事

  • 定义匿名类main::{lambda(int)#1}
  • 参考lambda表达式的内容,重载main::{lambda(int)#1}的函数调用操作符operator()
  • 为匿名类main::{lambda(int)#1}分配一个实例,并赋值给变量trans
  • 调用实例trans的函数调用操作符operator()

所以lambda表达式实际上是语法更为简洁的函数对象,它的特殊部分捕获列表实际上是传给函数对象的成员变量

  • 按值捕获:相当于按值传参,函数对象持有的是这个变量的拷贝。
  • 按引用捕获:相当于按引用传参,函数对象持有的是这个变量的引用。
  • 捕获列表和形参列表的区别在于 捕获列表捕获到的变量由函数对象的成员变量维护,生命周期于函数对象一样;而形参列表的入参生命周期是函数级别的。

避坑指南

static修饰的lambda表达式不能按引用捕获局部变量。

例如

std::string GetString() {
    std::string id = "Test";
    static auto appendId = [&id](std::string value) {
        return name + "-" + value;
    };
    std::string res = appendId("Hello");
}

int main() {
    std::string res1 = GetString();
    std::string res2 = GetString();    // 可能引发奔溃
}

原因很简单,lambda表达式appendId按引用捕获局部变量id,所以appendId会持有id的引用,但是因为appendIdstatic修饰的所以生命周期比局部变量id长,当id销毁以后appendId持有的id引用实际上已经销毁了,出现未定义的行为。

(其他坑待补充…)

扩展知识

函数对象(仿函数)

函数对象,也叫仿函数。如果一个类重载了函数调用操作符operator(),这个类的实例就叫函数对象,可以像使用函数一样使用这个对象。例如

Struct Transform {
    int operator()(int value) {
        ++count;
        return 100 + value;
    }

    int count = 0;
};

int main() {
    Transform trans;
    int res = trans(5);
}

Transform的实例trans就是一个函数对象,我们可以像调用函数一样使用trans。函数对象的特殊之处在于可以保持状态,例如trans可以通过成员变量count统计自己被调用过多少次,而普通函数做不到这一点。

  • 26
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值