一、什么是 Lambda?
C++ 11 加入了一个非常重要的特性 ——Lambda 表达式。营里(戴维营)的兄弟都对 Objective-C 很熟悉,许多人多 block 情有独钟,将各种回调函数、代理通通都用它来实现。甚至有人选择用 FBKVOController、BlocksKit 等开源框架将 KVO、控件事件处理都改为通过 block 解决。原因就是简单、方便、直观,函数的定义和使用出现在同一个地方。这里的 Lambda 表达式实际上和 block 非常类似,当然如果你用它和 Swift 语言的闭包比较,那就是一回事了。
这是一个关于 C\C++ 程序员的一个小故事,关于 C++11—— 刚刚通过的新标准的一个小故事…请不要误会,题目中所提及的 “优化” 并不是提升程序的性能 ——Lambda 表达式干不了这个。从本质上来说,它只是一种 “语法糖” 而已。不使用这种表达式,我们照样可以写出满足需求的程序。正如放弃 C 而使用汇编,或者放弃汇编而使用机器语言一样,你能控制的范围就在那里,不增不减。但如果有得选择,我相信大部分人会选择汇编而非机器语言,选择 C 而非汇编,甚至选择 C++ 而非 C 语言……。如果你确实是这样选择的,那么我有理由相信,你会选择 C++ 新标准中的 Lambda 表达式,因为它确实能够简化你的程序,让你写起程序来更容易;让你的程序更易读,更优美;同时也让你有更多向同行炫耀的资本。
从一个实际的应用说起
无论是 C 语言的使用者,还是 C++ 的用户,如果你从事 PC 程序的算法开发,我有 96.57% 的把握认为你可能使用过 C++ 标准模板库 STL(其中的 string,vector 之类)。毕竟,STL 的抽象不错,不用白不用,是不是。STL 中有一大类是算法,这些算法的抽象同样不错,我们就拿排序算法(sort)来说事吧。
假设现在有一个结构称为 Student,其中包含了 ID 与 name 两项 —— 分别表示学号与姓名。在某个应用中,用户希望对一个 Student 的数组按照 ID 的从大到小排序,那么程序可能写成如下的形式(本文中的所有程序均在 Visual Studio 2010 下编译通过):
#include <iostream>
using namespace std;
#include<string>
#include<algorithm>
struct Student
{
unsigned ID;
string name;
Student(unsigned i,string n):ID(i),name(n){}
};
struct compareID
{
bool operator()(const Student& val1,const Student& val2 )const
{
return val1.ID < val2.ID;
}
};
int main()
{
Student a[] = {Student(2,"john"),Student(0,"tom"),Student(1,"lili")};
sort(a,a+3,compareID());
for (int i = 0;i<3;++i)
{
cout<<a[i].name<<endl;
}
}
程序用 sort 进行排序,之后用一个 for 循环输出结果。而之所以能完成这个排序,则是由于仿函数 compardID 的存在。
现在假设用户的需求变了(或者是另一个需求),需要你按照学生的姓名进行排序,那么你需要重新写一个仿函数如下:
struct compareName {
bool operator ()(const Student& val1, const Student& val2) const
{
return val1.name < val2.name;
}
};
问题出现了,你意识到了吗?你只是想表达一个很简单的排序方式,确不得不引入很多的代码行来建相应的仿函数。如果这个函数在很多地方都会用到,那么建立它的价值还相对较大。如果只是用在一个地方,你也不得不中段你流畅是思路,一边骂娘一边写出这么多行代码。另一方面,程序的读者在读到相应部分的时候,也不得不中段他流畅的思路,在工程的某个地方苦苦求索 ——compareName 或者 compareID 是怎么干的呢?
是的,是的,作为一个 C++ 老鸟,你会说,这样写代码太不专业了。完全可以有不建立仿函数的写法,比如以 ID 排序时,完全可以通过引入 boost 库中的 bind 来实现,比如这样:
sort(a, a+3, bind(less<unsigned>(), bind(&Student::ID, _1), bind(&Student::ID, _2)));
如果你能写出或是读懂这段代码,我承认你的 C++ 水平确实说得过去(如果读不懂,没关系,它不是本文的重点)。但这段代码真的好吗?确实,这样可以省略了仿函数。但问题是代码的复杂性大大增加了 —— 即使如此简单的一个需求,bind 表达式也要复杂如斯,更复杂一点的需求要写成何等复杂的形式啊,这对于 bind 本身,写程序的人,读程序的人都是一种折磨 —— 你 hold 住吗?
如果用 Lambda 表达式呢,唔,这个 sort 语句可以这么写:
Student a[] = {Student(2,"john"),Student(0,"tom"),Student(1,"lili")};
sort(a,a+3,[](const Student&val1,const Student&val2){return val1.ID<val2.ID;});
for_each(a,a+3,[](const Student&val){cout<<val.ID<<" "<<val.name<<endl;});
二、Lambda表达式详解
[capture](parameters) mutable ->return-type{statement}
1.[capture]:捕捉列表。捕捉列表总是出现在 Lambda 函数的开始处。实际上,[] 是 Lambda 引出符。编译器根据该引出符判断接下来的代码是否是 Lambda 函数。捕捉列表能够捕捉上下文中的变量以供 Lambda 函数使用;
2.(parameters):参数列表。与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号 “()” 一起省略;
3.mutable:mutable 修饰符。默认情况下,Lambda 函数总是一个 const 函数,mutable 可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空);
4.->return-type:返回类型。用追踪返回类型形式声明函数的返回类型。我们可以在不需要返回值的时候也可以连同符号”->” 一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导;
5.{statement}:函数体。内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。
与普通函数最大的区别是,除了可以使用参数以外,Lambda 函数还可以通过捕获列表访问一些上下文中的数据。具体地,捕捉列表描述了上下文中哪些数据可以被 Lambda 使用,以及使用方式(以值传递的方式或引用传递的方式)。语法上,在 “[]” 包括起来的是捕捉列表,捕捉列表由多个捕捉项组成,并以逗号分隔。捕捉列表有以下几种形式:
1.[var] 表示值传递方式捕捉变量 var;
2.[=] 表示值传递方式捕捉所有父作用域的变量(包括 this);
3.[&var] 表示引用传递捕捉变量 var;
4.[&] 表示引用传递方式捕捉所有父作用域的变量(包括 this);
5.[this] 表示值传递方式捕捉当前的 this 指针。
上面提到了一个父作用域,也就是包含 Lambda 函数的语句块,说通俗点就是包含 Lambda 的 “{}” 代码块。上面的捕捉列表还可以进行组合,例如:
1.[=,&a,&b] 表示以引用传递的方式捕捉变量 a 和 b,以值传递方式捕捉其它所有变量;
2.[&,a,this] 表示以值传递的方式捕捉变量 a 和 this,引用传递方式捕捉其它所有变量。
不过值得注意的是,捕捉列表不允许变量重复传递。下面一些例子就是典型的重复,会导致编译时期的错误。例如:
3.[=,a] 这里已经以值传递方式捕捉了所有变量,但是重复捕捉 a 了,会报错的;
4.[&,&this] 这里 & 已经以引用传递方式捕捉了所有变量,再捕捉 this 也是一种重复。