Modern C++特性:lambda表达式

Lambda表达式就是匿名函数,在C++11之前Boost凭借C++语言强大的template和预处理宏,以及库作者强悍的奇技淫巧实现了Boost.lambda和更高级用法Boost.phoenix,但没有语言层面的支持,完全用库实现不但稍显累赘,而且代码观感不佳。C++11在语言层面实现了lambda,语法定义如下:

  1. [capture](parameters)mutable -> return-value { statement }

  • [capture]:捕捉列表,可以捕捉上下文中的变量以供lambda表达式使用。

  • (parameters):参数列表,同普通函数相同的参数列表定义,如果没有参数,可以留空括号 (),甚至连空括号都省略。在C++11中lambda参数类型不能自动推导,即不能为auto,在C++14中已经支持。而且C++14 起,lambda 能拥有自身的默认实参,诸如 [](int i = 6) { return i + 4; }

  • mutable:默认情况下lambda表达式是一个 const函数,但可以显式指定 mutable取消其常量性,这时不能省略空参数列表的空括号 ()

  • -> return-value:返回类型,如果没有返回类型(即 void),可以全部省略。如果返回类型明确,也可以省略,让编译器进行自动类型推导。

  • { statement }:函数体,跟普通函数相同的用法。

所以一个最简单的lambda表达式是这样的:

  1. []{}

尽管它什么事都没干,也没什么作用,但确实是一个合法的lambda表达式。

捕捉列表可以有0个,或1个,或多个捕捉项,以逗号分隔,C++11中可以有以下几种形式:

  1. [var],表示值传递方式捕捉变量 var

  2. [&var],表示引用传递方式捕捉变量 var

  3. [=],表示值传递方式捕捉所有父作用域的变量。

  4. [&],表示引用传递方式捕捉所有父作用域的变量。

  5. [this],表示捕捉当前 this指针,C++11/C++14不能捕捉 *this,但在C++17中已经可以捕捉,这解决了当前对象因为引用捕捉导致对象生命周期强制限制的问题。 this也包括在 [=]和 [&]中。

C++14中新增了初始化列表的捕捉变量形式:

  1. [var=expr],表示值传递方式捕捉变量 var,而且 var以表达式 expr进行初始化。

  2. [&var=expr],表示引用传递方式捕捉变量 var,而且 var以表达式 expr进行初始化。

此类捕捉变量的行为如同它声明并显式捕捉一个以类型 auto声明的变量,该变量的声明区是 lambda表达式体(即它不在其初始化列表的作用域中),但:

  1. 若以复制捕获,则闭包对象的非静态数据成员是指代这个 auto 变量的另一种方式。

  2. 若以引用捕获,则引用变量的生存期在闭包对象的生存期结束时结束。

这可用于以如 x = std::move(x)这样的捕获符捕获仅可移动的类型。这也使得以 const引用进行捕获称为可能,比如以 &cr = std::as_const(x)或类似的方式。

然后可以多个捕捉项组合:

  1. [=, &a, &b]

  2. [&, a, b]

  3. [i, x=x]

  4. [i, &x=x]

  5. [&r = x, x = x + 1]

但是多个捕捉项不能重复,任何捕获符只可以出现一次:

  1. [a, a] // 错误:a 重复

  2. [=, a] // 错误:a 重复

  3. [&, &a] // 错误:a 重复

  4. [this, *this] // 错误:"this" 重复 (C++17)

所以当默认捕获符是 &时,后继的简单捕获符必须不以 &开始。当默认捕获符是 =时,后继的简单捕获符必须以 & 开始,或者为 *this (自C++17 起) 或 this(自C++20 起)。例如:

  1. [&, &i] {}; // 错误:以引用捕获为默认时的以引用捕获

  2. [=, *this]{}; // C++17 前:错误:无效语法

  3. // C++17 起:OK:以复制捕获外围的 S2

  4. [=, this] {}; // C++20 前:错误:= 为默认时的 this

  5. // C++20 起:OK:同 [=]

捕捉列表的不同,效果也会不同:

  • 按值传递的捕捉项在lambda表达式被定义时就已经决定。

  • 按引用传递的捕捉项在lambda表达式被调用时决定。

  • 按值传递的捕捉变量不能被lambda表达式内修改,按引用传递的可以。

  • 从代码生成角度看,如果是内建数据类型(int,short,long之类的)如果以引用传递方式会比以值传递方式多一条获取变量地址的指令。

Lambda表达式在功能上跟仿函数(functor,也称函数对象,function object)非常相似:可以保存外部变量的状态,可以传入参数,可以被调用。编译器在实现lambda表达式时也采用了与仿函数相似的方法。

每个lambda表达式都有自己特有的类型,也就是说不能仅仅因为捕捉列表、参数列表、返回值类型相同而把一个lambda表达式赋给另一个保存着lambda表达式的变量,却可以把一个保存了lambda表达式的变量赋给另一个变量:

  1. auto f = [](int n)->int { return n;};

  2. decltype(f) f2 = f; // 正确

  3. decltype(f) f3 = [](int n)->int { return n;}; // 编译错误

但是lambda可以存储在 std::function中,例如:

  1. std::function<int(int)> f2 = [](int n) { return n; };

Lambda表达式在C++中最典型的应用场景是作为被回调体,比如STL诸多算法需要提供谓词(predicate),简短的lambda比函数指针和仿函数都要更适合承担这份工作:

  • 有机会被内联优化,函数指针不行。

  • 就地定义、就地使用,短小、分散的仿函数破坏程序整体结构。

但是lambda表达式并不能完全取代仿函数:

  • 函数体较大时,使用仿函数更能理清程序结构。

  • 捕捉变量范围有限,仅在父作用域范围,尽管有的编译器(GCC可以捕捉到全局变量等)自行扩展了范围,但并不合标准定义。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值