1. 引言
最近刷Leetcode经常看discuss,通常是佩服别人算法漂亮。但做第373题Find K Pairs with Smallest Sums时,被这个答案语言上的优雅震惊了。代码片段如下:
auto cmp = [&nums1, &nums2](pair<int, int> a, pair<int, int> b) {
return nums1[a.first] + nums2[a.second] >
nums1[b.first] + nums2[b.second];
};
priority_queue<pair<int, int>, vector<pair<int, int>>,
decltype(cmp)> min_heap(cmp);
通过使用auto、decltype和Lambda表达式等C++ 11新特性,大大压缩了代码量,降低了编写和理解难度,所以决定花时间好好研究一下Lambda表达式。
2. 什么是Lambda表达式?
查Lambda表达式资料时很容易被函数闭包、Lambda演算、形式系统这些深奥名词淹没而放弃学习,其实Lambda表达式就是匿名函数(annoymous function)——允许我们使用一个函数,但不需要给这个函数起名字。还是有点难懂?没关系,看完下面这个例子就清楚了。
int main() {
vector<int> data;
for (int i = 0; i < 10; ++i)
data.push_back(i);
sort(data.begin(), data.end());
for (int i = 0; i < data.size(); ++i)
cout << data[i] << endl;
return 0;
}
这段代码的含义是初始化data,对data里的元素排序后输出。algorithm库里的sort默认采用升序,想用倒序怎么办呢?对,自己定义一个比较函数cmp,作为参数传给sort:
bool cmp(int &a, int &b);
int main() {
vector<int> data;
for (int i = 0; i < 10; ++i)
data.push_back(i);
sort(data.begin(), data.end(), cmp);
for (int i = 0; i < data.size(); ++i)
cout << data[i] << endl;
return 0;
}
bool cmp(int &a, int &b) {
return a > b;
}
在定义了函数bool cmp(int &a, int &b)后,相同的函数签名变得不可用,我不能再用bool cmp(int &a, int &b)这个签名定义一个别的比较函数:
bool cmp(int &a, int &b) {
return (a % 3) > (b % 3);
}
问题是排序这件事通常不会反复做,那么用cmp比较大小是个一次性的临时需求,排序之后它的任务就已经完成了。所以给它特意起个名字污染命名空间似乎有点不太合算,可不可以不给它起cmp这个名字,又能使用比较大小的功能呢?答案当然是可以的,通过与cmp等价的匿名函数:
int main() {
vector<int> data;
for (int i = 0; i < 10; ++i)
data.push_back(i);
sort(data.begin(), data.end(), [](int &a, int &b)->bool {
return a > b;
});
for (int i = 0; i < data.size(); ++i)
cout << data[i] << endl;
return 0;
}
其中
[](int &a, int &b)->bool {
return a > b;
}
就是传说中的Lambda表达式了,先不管[]部分,(int &a, int &b)->bool表示接受两个int引用类型的参数,返回值是bool类型,{}里是函数体,是不是很简单?
关于Lambda表达式的意义可以参考知乎上的提问,我自己的理解是Lambda表达式实现了函数名字和功能的分离,允许在不起名字的情况下定义和使用功能。举个生活中的例子,点外卖时我们关心的只是外卖小哥送货上门的“功能”,不需要特意记住他的“名字”。
3. 怎么用Lambda表达式?
Lambda表达式的具体语法可以参考cppreference上的Guide。一个Lambda表达式的形式通常为:
[ capture-list ] ( params ) -> ret { body }
其中( params ) -> ret定义了这个匿名函数的参数和返回类型, { body }定义了这个匿名函数的功能,捕捉列表[ capture-list ]是做什么的呢?概括地讲,它使这个匿名函数可以访问外部(父作用域)变量。
还是举个例子:
int main() {
int a = 0;
auto f = ([]()->void {cout << a << endl;});
f();
return 0;
}
这段代码的含义是定义了一个匿名函数赋给f并运行f,但编译时会报错:
error: 'a' is not captured
因为变量a在函数f的外部,想要访问a的话需要把它加到[ capture-list ]里,也就是:
int main() {
int a = 0;
auto f = ([a]()->void {cout << a << endl;});
f();
return 0;
}
捕捉方式有按值和按引用两种。比如[a, &b]表示按值捕捉a,按引用捕捉b;[&, a]表示按引用捕捉所有父作用域变量,除了a按值捕捉,[=,&b]表示按值捕捉所有父作用域变量,除了b按引用捕捉。
假设有数组data,想生成只保留data中偶数的新数组result,可以用:
int main() {
vector<int> data;
vector<int> result;
for (int i = 0; i < 10; ++i)
data.push_back(i);
for_each(data.begin(), data.end(), [&result](int &elem){
if (elem % 2 == 0)
result.push_back(elem);
});
for_each(result.begin(), result.end(), [](int &elem){
cout << elem << endl;
});
return 0;
}
4. 参考资料
- What is a lambda expression in C++11?
http://stackoverflow.com/questions/7627098/what-is-a-lambda-expression-in-c11 - Lambda Functions
http://en.cppreference.com/w/cpp/language/lambda