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 f2 = [](int n) { return n; };

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

  • 有机会被内联优化,函数指针不行。
  • 就地定义、就地使用,短小、分散的仿函数破坏程序整体结构。

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

  • 函数体较大时,使用仿函数更能理清程序结构。
  • 捕捉变量范围有限,仅在父作用域范围,尽管有的编译器(GCC可以捕捉到全局变量等)自行扩展了范围,但并不合标准定义。

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