Modern C++特性:lambda表达式

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

[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. 若以引用捕获,则引用变量的生存期在闭包对象的生存期结束时结束。

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

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

[=, &a, &b]
[&, a, b]
[i, x=x]
[i, &x=x]
[&r = x, x = x + 1]

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

[a, a]  // 错误:a 重复
[=, a]  // 错误:a 重复
[&, &a]  // 错误:a 重复
[this, *this]  // 错误:"this" 重复 (C++17)

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

[&, &i] {};     // 错误:以引用捕获为默认时的以引用捕获
[=, *this]{};   // C++17 前:错误:无效语法
// C++17 起:OK:以复制捕获外围的 S2
[=, this] {};   // C++20 前:错误:= 为默认时的 this
// C++20 起:OK:同 [=]

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

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

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

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

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

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

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

auto f = [](int n)->int { return n;};
decltype(f) f2 = f; // 正确
decltype(f) f3 = [](int n)->int { return n;};  // 编译错误

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

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

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

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

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

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

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

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

觉得本文不错的话,分享一下给小伙伴吧~

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章