前言
在很多编程语言中,lambda非常常见,比如我之前熟悉的Kotlin语言,就把lambda和函数看成一等公民,在源码和官方demo中都有大量使用。这就造成了两个极端,如果不熟悉lambda的话,看这种代码就是煎熬,但是如果熟悉lambda以及其本质的话,lambda的使用可以大大提高可读性。
而在C++中,lambda使用的场景还不是非常多,本篇文章就来简单看一下日常使用,后续文章继续更新lambda深层次的本质原理。
正文
我们以最常见的排序算法来看,一步一步介绍为什么有lambda,以及简单使用。
谓词(predicate)
我们先来看一个简单的排序代码,调用默认的sort
函数:
C++
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
int main() {
std::vector<std::string> words = {"my", "name", "is", "jack", "six", "years"};
std::cout << "words:" << std::endl;
for (auto const &word : words) {
std::cout << word << std::endl;
}
//默认排序
sort(words.begin(), words.end());
std::cout << "default sort:" << std::endl;
for (auto const &word : words) {
std::cout << word << std::endl;
}
}
在这个例子中,调用sort
方法进行排序,默认使用的是字典序 ,即按照单词首字母进行排序。这时想实现按照其他规则进行排序,比如按照单词的长度进行排序,这时可以给sort
传递第三个参数,这个参数就是比较条件,同时这个参数有一个专业术语,叫做谓词(predicate
)。
谓词是一个可调用的表达式,其返回结果是一个能用作条件的值 。标准库算法所使用的谓词分为2类:一元谓词(unary predicate
)和二元谓词(binary predicate
),也就是接受单一和两个参数的区别。接受谓词参数的算法,对输入序列中的元素调用谓词 ,所以元素类型必须能够转换为谓词的参数类型。
比如下面代码,我们定义一个isShorter
方法,就可以替换默认sort
版本中的比较规则,代码如下:
C++
bool isShorter(const std::string &s1, const std::string &s2) {
return s1.size() < s2.size();
}
int main() {
std::vector<std::string> words = {"my", "name", "is", "jack", "six", "years"};
std::cout << "words:" << std::endl;
for (auto const &word : words) {
std::cout << word << std::endl;
}
//默认排序
sort(words.begin(), words.end());
std::cout << "default sort:" << std::endl;
for (auto const &word : words) {
std::cout << word << std::endl;
}
//使用isShorter作为谓词排序
sort(words.begin(), words.end(), isShorter);
std::cout << "isShorter sort:" << std::endl;
for (auto const &word : words) {
std::cout << word << std::endl;
}
}
在这种情况下就可以利用新的规则,进行字符串长短排序。这里定义了一个isShorter
函数,接受2个参数,进行字符串长度比较,这就是一个二元谓词。然后将之作为参数传递给sort
方法,sort
方法能正常运行的前提是words
中的元素可以转换为std::string
类型,并且对每个元素都调用谓词。从语法来看,这里很像是传入一个函数指针。
lambda表达式
根据算法接受一元谓词还是二元谓词,我们传递给算法的谓词必须严格接受一个或者两个参数 ,比如上面的isShorter
方法就必须是两个参数。但是,有时我们期望能操作更多的参数。比如现在加个需求:求大于等于一个给定长度的单词有多少。
可以先想一下如何实现,如果只是遍历集和,当然可以,但是我们这里利用先前的知识,可以先把vector
排序,然后找到第一个大于或者等于给定长度的字符串,这样后面的字符串都是符合的。伪代码思路如下:
C++
void biggies(std::vector<std::string> &words,
std::vector<std::string>::size_type sz) {
sort(words.begin(), words.end(), isShorter);
std::cout << "使用isShorter谓词排序后:" << std::endl;
for (auto const& word : words) {
std::cout << word << std::endl;
}
// 获取一个迭代器,指向第一个满足size() >= sz的元素
// 计算满足size() >= sz的元素的数目
// 打印长度大于给定值的单词
}
现在我们的问题就是找到第一个符合条件的元素即可,我们可以使用标准库的find_if
算法来查找第一个具有特定大小的元素。
find_if
算法接受一对迭代器,表示一个范围,同时第三个参数是一个谓词。find_if
算法对输入序列中的每个元素都调用给定的谓词,返回第一个使得谓词返回非0值的元素,如果不存在,返回尾迭代器。
类似前面的sort
用法,我们可以编写一个函数,把它当做谓词,代码如下:
C++
bool shorterSz(const std::string &s,
std::vector<std::string>::size_type sz) {
return s.size() >= sz;
}
可惜的是,这个谓词并不能传递给find_if
,因为find_if
接受一元谓词,我们传递给find_if
的函数必须严格接受一个参数。
lambda简介
在这种情况下,我们可以使用lambda来解决这种情况。我们可以向一个算法传递任何类别的可调用对象(callable object
),对于一个对象或者一个表达式,如果可以对其调用运算符()
,则可以称为可调用的。在C++中,函数和函数指针肯定是可调用对象,其次就是重载了函数调用运算符的类以及本章将要说的lambda表达式。
一个lambda表达式表示一个可调用的代码单元 ,可以理解为一个未命名的内联函数 。与普通函数类似,一个lambda也具有返回类型、参数列表和函数体 。与普通函数不同的点是lambda可以定义在函数内部。
标准的lambda表达形式如下形式:
[caputre list](parameter list) -> return type { function body}
我们给拆开来看:
capture list
为捕获列表,啥意思呢?前面说了,lambda可以定义在函数中,而lambda同时可以使用所在函数中定义的局部变量,想使用哪些变量,是值拷贝还是引用,这个就由捕获列表来完成。parameter list
、return type
和function body
就和普通函数一样,区别就是lambda必须使用尾置返回,即返回类型定义在参数列表之后。
上面基础概念非常重要,对于lambda可以忽略参数列表和返回类型,这时就是无参函数,类型可以自动推导,比如下面代码:
C++
auto f = [] { return 42; };
这里的f
是一个可调用对象,我们可以使用调用运算符来调用:
C++
cout << f();
通过这个简单的例子可以看出,lambda的本质是可调用对象,定义来看像是函数的缩写,但是比普通函数多一个捕获列表。
向lambda传递参数
既然类似普通函数,所以调用一个lambda时,必须给定实参来初始化lambda的形参。注意点是,lambda不能有默认参数,一旦形参初始化完毕,就可以执行函数体了。
回顾前面的isShorter
方法,我们可以定义一个lambda表达式来完成一样的工作:
C++
auto f = [] (const std::string &s1, const std::string &s2) { return s1.size() < s2.size(); };
sort(words.begin(), words.end(), f);
for (auto const& word : words) {
std::cout << word << std::endl;
}
我们把f当做谓词传递给sort
时,当sort
需要比较元素时,就会调用f
这个lambda表达式,和前面调用isShorter
方法一样,会传递参数给lambda表达式。
使用捕获列表
现在我们可以解决前面所说的find_if
的问题了,我们想对find_if
传入如下函数:
C++
bool shorterSz(const std::string &s,
std::vector<std::string>::size_type sz) {
return s.size() >= sz;
}
经过发现我们可知,这里的sz
其实是方法biggies
的局部变量,我们再来看一下该方法:
C++
void biggies(std::vector<std::string> &words,
std::vector<std::string>::size_type sz) {
sort(words.begin(), words.end(), isShorter);
std::cout << "使用isShorter谓词排序后:" << std::endl;
for (auto const& word : words) {
std::cout << word << std::endl;
}
// 获取一个迭代器,指向第一个满足size() >= sz的元素
// 计算满足size() >= sz的元素的数目
// 打印长度大于给定值的单词
}
这样我们就可以使用lambda表达式来替代原来我们想实现的函数 ,以及使用捕获来获取biggies
中的局部变量,这样就可以达到目的:即可以使用外面方法的局部变量,又定义了一个一元谓词,话不多说,直接看代码:
C++
void biggies(std::vector<std::string> &words,
std::vector<std::string>::size_type sz) {
auto f = [] (const std::string &s1, const std::string &s2) { return s1.size() < s2.size(); };
sort(words.begin(), words.end(), f);
std::cout << "使用isShorter谓词排序后:" << std::endl;
for (auto const& word : words) {
std::cout << word << std::endl;
}
// 获取一个迭代器,指向第一个满足size() >= sz的元素
auto wc = find_if(words.begin(), words.end(), [sz](const std::string &s){
return s.size() >= sz;
});
// 计算满足size() >= sz的元素的数目
// 打印长度大于给定值的单词
std::cout << "长度大于" << sz << "的字符串:";
for_each(wc, words.end(), [](const std::string &s){
std::cout << s << " ";
});
}
int main() {
std::vector<std::string> words = {"fox", "jumps", "over", "quick", "red", "slow", "the", "turtle"};
biggies(words, 4);
}
//输出
使用isShorter谓词排序后:
fox
red
the
over
slow
jumps
quick
turtle
长度大于4的字符串:over slow jumps quick turtle
在上面代码中,我们传递给find_if
的是一个lambda表达式,它的参数只有一个,符合一元谓词的定义。同时又捕获了外部函数中的sz
变量,从而达到我们想要的结果。
最后使用for_each
函数来打印符合的字符串,同样思考一下,for_each
的作用是对每一个集和中的元素进行处理,所以它接收一个一元谓词,参数类型是集和元素类型,所以我们可以使用lambda来替代这个匿名函数。
总结
本篇文章简单介绍了lambda,总结如下:
- 谓词是一个非常重要的概念,在所有语言和集和相关的API中都会很常见,谓词是一个可调用的表达式,返回结果是可以用作条件判定的值,对于标准库中的算法来说,会把每个元素当成参数来调用这个谓词。
- 标准库算法中,比如
sort
、find_if
等,我们要明确算法的作用,这样才能理解和写出符合算法的谓词,比如参数类型与个数的区别。 - lambda也是一个可调用对象,现在阶段可以看成是一个未命名的函数,我们写的方法都可以转成lambda格式。
- lambda比普通函数更便捷的点是,它可以捕获外部函数的局部变量,这样就完美解决了谓词参数个数的限制问题了。
后续文章,我们详细来探究lambda的本质是什么,捕获的方式有没有其他形式等。