从软件开发的角度看,以lambda概念为基础的“函数式变成”(Functional Programming)是与命令式编程(Imperative Programming)、面向对象编程(Object-oriented Programming)等并列的一种编程范型(Programming Paradigm)。从最早基于命令式编程范式的语言C,到加入了面向对象编程范式的血统C++,再到逐渐融入函数式编程范式的lambda的新语言规范C++11,C/C++的发展也在融入多范型支持的潮流中。
#include
using namespace std;
int main() {
int girls = 3, boys = 4;
auto totalChild = [](int x,int y)->int{return x+y;};
cout<return 0;
}
lambda函数的语法定义如下:
[capture] (parameters) mutable ->return_type{statement}
其中:
在lambda函数的定义式中,参数列表和返回类型都是可选部分,而捕捉列表和函数体都可能为空,C++中最简单的lambda函数只需要声明为:
[]{};
理所当然,该lambda函数不能做任何事情。
语法上,捕捉列表由多个捕捉项组成,并以逗号隔开。捕捉列表有如下几种形式:
通过一些组合,捕捉列表可以表示更复杂的意思。比如:
不过值得注意的是,捕捉列表不允许变量重复传递。下面以下例子就是典型的复合,会导致编译时期的错误:
在C++11之前,我们在使用STL算法时,通常会使用到一种特别的对象,一般来说,我们称之为函数对象,或者仿函数(functor)。仿函数简单地说,就是重定义了成员函数operator()的一种自定义类型对象。这样的对象有个特点:其使用在代码层面感觉跟函数的使用并无二样,但究其本质却并非函数。
我们来看一个例子:
#include
using namespace std;
class AirportPrice{
float _dutyfreerate;
public:
AirportPrice(float rate):_dutyfreerate(rate){}
float operator()(float price){
return price * (1 - _dutyfreerate/100);
}
};
int main() {
float tax_rate = 5.5f;
AirportPrice Changi(tax_rate);
//定义lambda函数,捕捉tax_rate变量
auto Changi2 = [tax_rate](float price)->float{return price * (1 - tax_rate/100);};
//用仿函数计算扣税后的价格
float purchased = Changi(3699);
//用lambda函数计算扣税后的价格
float purchased2 = Changi2(2899);
cout<cout<return 0;
}
上述代码是一个机场返税的例子。该例中,分别用了仿函数和lambda两种方式来完成扣税后的产品价格计算。在这里我们看到,lambda函数捕捉了tax_rate变量,而仿函数则以tax_rate初始化类。其他的,如在参数传递上,两者保持一致。可以看到,除去在语法层面上的不同,lambda和仿函数却有着相同的内涵——可以捕捉一些变量作为初始状态,并接受参数进行运算。
而事实上,仿函数是编译器实现lambda的一种方式。在现阶段,通常编译器都会把lambda函数转化成为一个仿函数对象。因此C++11中,lambda可以视为仿函数的一个等价形式了,或者更动听地说,lambda是仿函数的“语法甜点”。
lambda对C++11最大的贡献,或者说是改变,应该在STL库中。更加具体的说,我们会发现使用STL算法更加容易了,也更加容易学习了。
首先我们看一个最为常见的STL算法for_each,其原型如下:
for_each(InputIterator beg, InputIterator end, UnaryProc op)
for_each算法需要一个标记开始的迭代器,一个标记结束的迭代器,以及一个接受单个参数的“函数”(即一个函数指针、仿函数或者lambda函数)。
for_each的一个示意实现如下:
for_each(iterator begin, iterator end, Function fn) {
for(iterator i = begin; i != end; ++i) fn(*i);
}
通过for_each,我们可以完成各种循环操作:
#include
#include
#include
using namespace std;
vector<int> nums;
vector<int> largeNums;
const int ubound = 10;
inline void LargeNumsFunc(int i) {
if (i > ubound) {
largeNums.push_back(i);
}
}
void Above() {
//传统的for循环
for(auto itr = nums.begin(); itr != nums.end(); ++itr) {
if(*itr > ubound)
largeNums.push_back(*itr);
}
//使用函数指针
for_each(nums.begin(), nums.end(), LargeNumsFunc);
//使用lambda函数和算法for_each
for_each(nums.begin(), nums.end(), [=](int i){
if (i > ubound)
largeNums.push_back(i);
});
}
我们用了三种方式遍历一个vector nums,找出其中大于ubound的值,并将其写入到另外一个vector largeNums中。第一种是传统的for循环;第二种,则更泛型地使用了for_each算法以及函数指针;第三种同样使用了for_each,但是第三个参数传入的时lambda函数。
(1)lambda函数与手写方式比较
Scott Mayer在Effective STL(item43)中提到:使用for_each算法比手写的循环在效率、正确性、可维护性上都有一定优势。程序员不用关心iterator,或者说循环的细节,只需要设定边界,作用于每个元素的操作,就可以在近似“一条语句”内完成循环,正如函数指针版本和lambda版本完成的那样。
(2)lambda函数与函数指针方式比较
函数指针的方式看似简洁,不过却有着很大的缺陷:第一点是函数定义在别的地方,这样的代码阅读起来并不方便;第二点则是出于效率考虑,使用函数指针很可能导致编译器不对其进行inline优化,再循环次数较多的时候,内联的lambda比没有内联的函数指针在性能方面强很多。因此,相比于函数指针,lambda有着无可替代的优势。此外,函数指针的应用范围型对狭小,尤其是我们需要具备一些运行时才能决定的状态时,函数指针就会捉襟见肘了。
C++98时代,遇到这种情况时,迫切想应用泛型编程的C++程序员或许会毫不犹豫地使用仿函数,不过现在我们则没有必要那么做:
#include
#include
#include
using namespace std;
vector<int> nums;
vector<int> largeNums;
const int ubound = 10;
class LNums{
public:
LNums(int u):ubound(u){}
void operator()(int i) const{
if(i > ubound)
largeNums.push_back(i);
}
private:
int ubound;
};
void Above() {
//使用仿函数
for_each(nums.begin(),nums.end(),LNums(ubound));
//使用lambda函数
for_each(nums.begin(), nums.end(), [=](int i){
if (i > ubound) {
largeNums.push_back(i);
}
});
}
(3)lambda函数与仿函数方式比较
在C++98时代,STL中内置了一些仿函数供程序员使用,所以我们需要知道这些内置仿函数的功能才能够使用它,对于没有太多接触STL算法的人可能对这种方式相当迷惑;反观lambda函数,其意义简洁明了,使用者使用的时候,也不需要有太多的背景知识。
在现有的C++11中,lambda函数并不是仿函数的完全替代者,这一点很大程度上是由lambda的捕捉列表的限制造成的。在现行的C++11标准中,捕捉列表仅能捕捉到父作用域的自动变量,而对超出这个范围的变量,是不能被捕捉的。而如果我们采用仿函数,则不会有这样的限制,更一般地讲,仿函数可以被定义以后在不同的作用域范围内取得初值。这使得仿函数天生具有跨作用域共享的特征。