C++中Lambda函数

  从软件开发的角度看,以lambda概念为基础的“函数式变成”(Functional Programming)是与命令式编程(Imperative Programming)、面向对象编程(Object-oriented Programming)等并列的一种编程范型(Programming Paradigm)。从最早基于命令式编程范式的语言C,到加入了面向对象编程范式的血统C++,再到逐渐融入函数式编程范式的lambda的新语言规范C++11,C/C++的发展也在融入多范型支持的潮流中。

C++11中的lambda函数

#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}
其中:

  • [capture]:捕捉列表。捕捉列表总是出现在lambda表达式的开始处。事实上,[]是lambda引出符。编译器根据该引出符判断接下来的代码是否是lambda函数。捕捉列表能够捕捉上下文中的变量供lambda函数使用。
  • (parameters):参数列表。与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号()一起省略。
  • mutable:mutable修饰符。默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空)。
  • ->return_type:返回类型。用追踪返回类型形式声明函数的返回类型。出于方便,不需要返回值的时候也可以连同符号->一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导。
  • {statement}:函数体。内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。

  在lambda函数的定义式中,参数列表和返回类型都是可选部分,而捕捉列表和函数体都可能为空,C++中最简单的lambda函数只需要声明为:

[]{};

  理所当然,该lambda函数不能做任何事情。
  语法上,捕捉列表由多个捕捉项组成,并以逗号隔开。捕捉列表有如下几种形式:

  • [var]表示值传递方式捕捉变量var
  • [=]表示值传递方式捕捉所有父类作用域变量(包括this)
  • [&var]表示引用传递方式捕捉变量var
  • [&]表示引用传递捕捉所有父类作用域的变量(包括this)
  • [this]表示值传递方式捕捉当前的this指针。

  通过一些组合,捕捉列表可以表示更复杂的意思。比如:

  • [=,&a,&b]表示以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量。
  • [&,a,this]表示以值传递的方式捕捉变量a和this,引用传递方式捕捉其他所有变量。

  不过值得注意的是,捕捉列表不允许变量重复传递。下面以下例子就是典型的复合,会导致编译时期的错误:

  • [=,a]这里=已经以值传递方式捕捉了所有变量,捕捉a重复。
  • [&,&this]表示以传递的方式捕捉变量a和this,引用传递方式捕捉其他所有变量。

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与STL

  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标准中,捕捉列表仅能捕捉到父作用域的自动变量,而对超出这个范围的变量,是不能被捕捉的。而如果我们采用仿函数,则不会有这样的限制,更一般地讲,仿函数可以被定义以后在不同的作用域范围内取得初值。这使得仿函数天生具有跨作用域共享的特征。

你可能感兴趣的:(C++11新特性)