C++11—lambda函数

【1】lambda表达式语法

lambda表达式的语法定义如下:

[capture](parameters)mutable ->return-type { statement };

(1)[capture]: 捕捉列表。捕捉列表总是出现在lambda函数的开始处。实质上,[]是lambda引出符(即独特的标志符)

编译器根据该引出符判断接下来的代码是否是lambda函数

捕捉列表能够捕捉上下文中的变量以供lambda函数使用

捕捉列表由一个或多个捕捉项组成,并以逗号分隔,捕捉列表一般有以下几种形式:

<1> [var] 表示值传递方式捕捉变量var

<2> [=] 表示值传递方式捕捉所有父作用域的变量(包括this指针)

<3> [&var] 表示引用传递捕捉变量var

<4> [&] 表示引用传递捕捉所有父作用域的变量(包括this指针)

<5> [this] 表示值传递方式捕捉当前的this指针

<6> [=,&a,&b] 表示以引用传递的方式捕捉变量 a 和 b,而以值传递方式捕捉其他所有的变量

<7> [&,a,this] 表示以值传递的方式捕捉 a 和 this,而以引用传递方式捕捉其他所有变量

备注:父作用域是指包含lambda函数的语句块

另外,需要注意的是,捕捉列表不允许变量重复传递。下面的例子就是典型的重复,会导致编译错误:

[=, a] 这里 = 已经以值传递方式捕捉了所有的变量,那么再捕捉 a 属于重复

[&,&this] 这里 & 已经以引用传递方式捕捉了所有变量,那么再捕捉 this 属于重复

(2)(parameters): 参数列表。与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号()一起省略

(3)mutable : mutable修饰符。默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性(后面有详解)

在使用该修饰符时,参数列表不可省略(即使参数为空)

(4)->return-type : 返回类型。用追踪返回类型形式声明函数的返回类型。

出于方便,不需要返回值的时候也可以连同符号->一起省略

此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导

(5){statement} : 函数体。内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量

在lambda函数的定义中,参数列表和返回类型都是可选的部分,而捕捉列表和函数体都可能为空

那么,在极端情况下,C++11中最为简单的lambda函数只需要声明为:

[]{};

就可以了。不过显然,这样的lambda函数不能做任何事情(乍一看好漂亮,其实仅是好看)。

【2】lambda函数示例代码

示例代码1:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 void main()
 5 {
 6     int a = 20, b = 10;
 7 
 8     auto totalAB = [] (int x, int y)->int { return x + y; };
 9     int aAddb = totalAB(a, b);
10     cout << "aAddb :" << aAddb << endl;
11 
12     auto totalAB2 = [a, &b]()->int { return a + b; };
13     int aAddb2 = totalAB2();
14     cout << "aAddb2 :" << aAddb2 << endl;
15 
16     auto totalAB3 = [=]()->int { return a + b; };
17     int aAddb3 = totalAB3();
18     cout << "aAddb3 :" << aAddb3 << endl;
19 
20     []{}; // 最简lambda函数
21     [=] { return a + b; }; // 省略了参数列表与返回类型,返回类型由编译器推断为int
22     auto fun1 = [&] (int c) { b = a + c; }; // 省略了返回类型,无返回值
23     auto fun2 = [=, &b](int c)->int { return b += a + c; }; // 各部分都很完整的lambda函数
24     cout << "fun2(100) :" << fun2(100) << endl;
25 }
26 // Result:
27 /*
28 aAddb :30
29 aAddb2 :30
30 aAddb3 :30
31 fun2(100) :130
32 */

以上代码仅供学习参考

【3】lambda函数的作用

lambda函数的使用示例代码:

 1 #include <iostream>
 2 #include <vector>
 3 #include <algorithm>
 4 #include "time.h"
 5 using namespace std;
 6 
 7 void main()
 8 {
 9     vector<int> nVec;
10     for (int i = 0; i < 100000; ++i)
11     {
12         nVec.push_back(i);
13     }
14 
15     double time_Start = (double)clock();
16     for (vector<int>::const_iterator it = nVec.begin(); it != nVec.end(); ++it) 
17     {
18         cout << *it << endl;
19     }
20     double time_Finish = (double)clock();
21     double time_Interval_1 = (double)(time_Finish - time_Start) / 1000;
22     
23     time_Start = (double)clock();
24     for_each( nVec.begin(), nVec.end(), [] (int val){ cout << val << endl; } );
25     time_Finish = (double)clock();
26     double time_Interval_2 = (double)(time_Finish - time_Start) / 1000;
27 
28     cout << "time_Interval_1 :" << time_Interval_1 << endl;
29     cout << "time_Interval_2 :" << time_Interval_2 << endl;
30 
31 }
32 // Result:
33 /*
34 time_Interval_1 :17.748
35 time_Interval_2 :17.513
36 */

lambda函数的引入为STL的使用提供了极大的方便。同样是遍历容器,效率反而提高了很多。

【4】lambda函数 与 仿函数

何谓仿函数?个人理解,像函数一样工作的对象。

根据面向对象的编程思想,那么问题来了!既然主语是一个对象,创建这个对象的类长什么样子呢?

据听说,所有科学中数学学科最重要,语文重要性次之。为什么呢?

数学可以利用来解决问题,但当问题解决不了的时候,可以用语文涂画,涂画得让人听不懂。好像很高大上一样一样~

关于仿函数,请看下面示例:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 class _functor_plus
 5 {
 6 private:
 7     int m_nValue;
 8 
 9 public:
10     _functor_plus(int nValue = 100);
11     _functor_plus operator+ (const _functor_plus & funObj);
12     void printInfo();
13 };
14 
15 _functor_plus::_functor_plus(int nValue) : m_nValue(nValue)
16 {
17 }
18 
19 _functor_plus _functor_plus::operator+ (const _functor_plus & funObj)
20 {
21     m_nValue += funObj.m_nValue;
22     return _functor_plus(m_nValue);
23 }
24 
25 void _functor_plus::printInfo()
26 {
27     cout << m_nValue << endl;
28 }
29 
30 class _functor_override
31 {
32 public:
33     int operator()(int x, int y);
34 };
35 
36 int _functor_override::operator()(int x, int y)
37 {
38     return x + y;
39 }
40 
41 int main()
42 {
43     _functor_plus plusA, plusB, plusC;
44     plusC = plusA + plusB;
45     plusA.printInfo();
46     plusB.printInfo();
47     plusC.printInfo();
48 
49     int boys = 4, girls = 3;
50     _functor_override totalChildren;
51     cout << "totalChildren(int, int): " << totalChildren(boys, girls);
52 }
53 // Result:
54 /*
55 200
56 100
57 200
58 totalChildren(int, int): 7
59 */

在这个例子中,_functor_override类的operator()被重载。

因此,在调用该函数的时候,我们看到与函数调用一样的形式。

只不过这里的totalChildren不是函数名称,而是一个对象名称。

相比于函数,仿函数可以拥有初始化状态:

一般通过class定义私有成员,并在声明对象的时候对其进行初始化,

那般,私有成员的状态就成了仿函数的初始状态。

由于声明一个仿函数对象可以拥有多个不同的初始状态的实例,

因此,可以借由仿函数产生多个功能类似实质却各不同的仿函数实例。

请参见下例:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 class Tax
 5 {
 6 private:
 7     double m_dRate;
 8     int m_nBase;
 9 
10 public:
11     Tax(double dRate, int nBase) : m_dRate(dRate), m_nBase(nBase)
12     {
13     }
14 
15     double operator() (double dMoney)
16     {
17         return (dMoney - m_nBase) * m_dRate;
18     }
19 };
20 
21 int main()
22 {
23     Tax high(0.40, 30000);
24     Tax middle(0.25, 20000);
25     cout << "tax over 3w: " << high(37500) << endl;
26     cout << "tax over 2w: " << middle(24000) << endl;
27 }
28 // Result:
29 /*
30 tax over 3w: 3000
31 tax over 2w: 1000
32 */

到这里,是否发现仿函数和lambda之间存在一种“衍生”的关系?

难道还不明显?没看懂?咱再接着剖析,谁让程序员就这么理性呢?

请再看下例:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 class AirportPrice
 5 {
 6 private:
 7     double m_dDutyfreeRate;
 8 
 9 public:
10     AirportPrice(double dDutyfreeRate) : m_dDutyfreeRate(dDutyfreeRate)
11     {}
12 
13     double operator() (double dPrice)
14     {
15         return dPrice * (1 - m_dDutyfreeRate/100);
16     }
17 };
18 
19 void main()
20 {
21     double dRate = 5.5;
22     AirportPrice fanFunObj(dRate);
23 
24     auto ChangLambda = [dRate](double dPrice)->double
25     {
26         return dPrice * (1 - dRate/100);
27     };
28     double purchased1 = fanFunObj(3699);
29     double purchased2 = ChangLambda(3699);
30     cout << "purchased1:" << purchased1 << endl;
31     cout << "purchased2:" << purchased2 << endl;
32 }
33 // Result:
34 /*
35 purchased1:3495.55
36 purchased2:3495.55
37 */

分别使用了仿函数和lambda两种方式来完成扣税后的产品价格计算。

lamba函数捕捉了dRate变量,而仿函数则以dRate进行初始化类。

其他的,在参数传递上,两者保持一致,结果也一致。

可以看到,除去在语法层面的差异,lambda函数和仿函数有着相同的内涵:

即都可以捕捉一些变量作为初始化状态,并接受参数进行运算。

而事实上,仿函数正是编译器实现lambda的一种方式。

在现阶段,通常编译器都会把lambda函数转化为一个仿函数对象。

因此,C++11中,lambda可以视为仿函数的一种等价形式。

备注:有时,编译时发现lambda函数出现了错误,编译器会提示一些构造函数相关的信息,

显然是由于lambda的这种实现方式造成的。理解这种实现也能够正确理解错误信息的由来。

【5】lambda函数等同于一个局部函数

局部函数,在函数作用域中定义的函数,也称为内嵌函数。

局部函数通常仅属于其父作用域,能够访问父作用域的变量。

C/C++语言标准中不允许局部函数存在(FORTRAN语言支持)

C++11标准却用比较优雅的方式打破了这个规则。

因为事实上,lambda可以像局部函数一样使用。请参见下例:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 extern int z = 100;
 5 extern float c = 100.00;
 6 
 7 void Calc(int& rnOne, int nTwo, float& rfThree, float fFour)
 8 {
 9     rnOne = nTwo;
10     rfThree = fFour;
11 }
12 
13 void TestCalc()
14 {
15     int x, y = 3;
16     float a, b = 4.0;
17     int success = 0;
18 
19     auto validate = [&]()->bool
20     {
21         if ((x == y + z) && (a == b + c))
22             return 1;
23         else
24             return 0;
25     };
26 
27     Calc(x, y, a, b);
28     success += validate();
29 
30     y = 1024;
31     b = 100.0;
32     Calc(x, y, a, b);
33     success += validate();
34 }
35 
36 void main()
37 {
38 }

在没有lambd函数之前,通常需要在TestCalc外声明同样一个函数,

并且把TestCalc中的变量当作参数进行传递。

出于函数作用域及运行效率的考虑,那样声明函数通常要加上关键字static 和 inline

相比于一个传统意义上的函数定义,lambda函数在这里直观,使用方便可读性很好。请参见下例:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 int Prioritize(int nValue)
 5 {
 6     return nValue + 10;
 7 }
 8 
 9 int  AllWorks(int nTimes)
10 {
11     int i = 0, x = 0;
12     try
13     {
14         for (i = 0; i < nTimes; ++i)
15         {
16             x += Prioritize(i);
17         }
18     }
19     catch (...)
20     {
21         x = 0;
22     }
23 
24     const int y = [=]()->int
25     {
26         int i = 0, val = 0;
27         try
28         {
29             for (; i < nTimes; ++i)
30             {
31                 val += Prioritize(i);
32             }
33         }
34         catch (...)
35         {
36             val = 0;
37         }
38         return val;
39     }();
40     // lambda表达式
41     {
42         []{}();
43         [](){}();
44         []{ cout << "emptyLambdaExec" << endl; }();
45         [=](){ cout << "const int y :" << y << endl; }();
46         [&](){ cout << "int x :" << x << endl; }();
47     }
48 
49     return 0;
50 }
51 
52 void main()
53 {
54     AllWorks(10);
55 }
56 
57 // Result:
58 /*
59 emptyLambdaExec
60 const int y :145
61 int x :145
62 */

备注:注意此例中的lambda表达式作用域中比较特殊的几个lambda函数。

【6】关于lambda的一些问题及其有趣的测试

(1)使用lambda函数时候,不同的捕捉方式会导致不同的结果:

请看下例:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 void main()
 5 {
 6     int j = 10;
 7     auto by_val_lambda = [=] { return j + 1; };
 8     auto by_ref_lambda = [&] { return j + 1; };
 9     cout << "by_val_lambda: " << by_val_lambda() << endl;
10     cout << "by_ref_lambda: " << by_ref_lambda() << endl;
11     ++j;
12     cout << "by_val_lambda: " << by_val_lambda() << endl;
13     cout << "by_ref_lambda: " << by_ref_lambda() << endl;
14 }
15 
16 //Result:
17 /*
18 by_val_lambda: 11
19 by_ref_lambda: 11
20 by_val_lambda: 11
21 by_ref_lambda: 12
22 */

充分说明了传值和引用方式的区别。

(2)使用lambda函数与函数指针

一般情况下,把匿名的lambda函数赋值给一个auto类型的变量,

这是一种声明和使用lambda函数的方法。

结合关于auto的知识,有人会猜测totalChild是一种函数指针类型的变量

结合lambda函数和仿函数之间关系,大多人会倾向于认为lambda是一种自定义类型。

实质上,lambda的类型并非简单函数指针类型或自定义类型。

从C++11标准定义发现,lambda类型被定义为“闭包”的类,而每一个lambda表达式则会产生一个闭包类型的临时对象。

也因此,严格地讲,lambda函数并非函数指针。

但是,C++11标准却允许lambda表达式向函数指针的转换,

前提是lambda函数没有捕捉任何变量,且函数指针所示的函数原型,必须跟lambda函数有着相同的调用方式。

 1 #include <iostream>
 2 using namespace std;
 3 
 4 void main()
 5 {
 6     int girs = 3, boys = 4;
 7     auto totalChild = [](int x, int y)->int{ return x + y; };
 8     typedef int (*pFunAll)(int x, int y);
 9     typedef int (*pFunOne)(int x);
10 
11     pFunAll funAll;
12 //  funAll = totalChild; // 编译失败!
13 
14     pFunOne funOne;
15 //  funOne = totalChild; //编译失败!参数必须一致
16 
17     decltype(totalChild) allPeople = totalChild; // 需通过decltype获得lambda的类型
18 //  decltype(totalChild) totalPeople = funAll; // 编译失败,指针无法转换lambda
19 }

第 12 行,编译错误信息如下:

error C2440: “=”: 无法从“`anonymous-namespace'::<lambda0>”转换为“pFunAll”  

         没有可用于执行该转换的用户定义的转换运算符,或者无法调用该运算符

MSVC10环境下,第一步编译不通过。

关于此问题参见文章《在 MSVC10 下,將 lambda expression 轉換成 C 的 function pointer

第 15 行 编译失败,参数不一致

第 18 行 编译失败,函数指针转换为lambda也是不成功的。

值得注意的是,可以通过decltype的方式获取lambda函数的类型。

(3)lambda函数的常量性以及mutable关键字

C++11中,默认情况下lambda函数是一个const函数。

神马意思呢?请参见下例:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 class const_val_lambda
 5 {
 6 public:
 7     const_val_lambda(int v) : m_nVal(v)
 8     {}
 9 public:
10     void operator() () const
11     {
12 //        m_nVal = 3;  /*注意:常量成员函数*/ 
13     }
14 
15     void ref_const_Fun(int& nValue) const
16     {
17         nValue = 100;
18     }
19 
20 private:
21     int m_nVal;
22 };
23 
24 void main()
25 {
26     int val = 10;
27     // 编译失败!在const的lambda中修改常量
28 //    auto const_val_lambda = [=]() { val = 3;}; // 不能在非可变 lambda 中修改按值捕获
29     // 非const的lambda,可以修改常量数据
30     auto mutable_val_lambda = [=]() mutable{ val = 3; };
31     // 依然是const的lambda,不过没有改动引用本身
32     auto const_ref_lambda = [&] { val = 3; };
33     // 依然是const的lambda,通过参数传递val
34     auto const_param_lambda = [&](int varA) { varA = 3;};
35     const_param_lambda(val);
36 }

备注:使用引用方式传递的变量在常量成员函数中修改值并不会导致错误。

【7】lambda 与 STL

lambda对C++11最大的贡献,或者说改变,应该在STL库中

相关应用,具体请再参见下例:

 1 #include <vector>
 2 #include <algorithm>
 3 #include <iostream>
 4 using namespace std;
 5 
 6 const int ubound = 3;
 7 
 8 vector<int> nums;
 9 vector<int> largeNums;
10 
11 void initNums()
12 {
13     for (int i = 1; i < 5; ++i)
14     {
15         nums.push_back(i);
16     }
17 }
18 
19 inline void largeNumsFunc(int i)
20 {
21     if (i > ubound)
22     {
23         largeNums.push_back(i);
24     }
25 }
26 
27 void filter()
28 {
29     for (auto it = nums.begin(); it != nums.end(); ++it)
30     {
31         if ((*it) > ubound)
32         {
33             largeNums.push_back(*it);
34         }
35     }
36 
37     for_each (nums.begin(), nums.end(), largeNumsFunc);
38 
39     for_each (nums.begin(), nums.end(), [=](int i)
40     {
41         if (i > ubound)
42         {
43             largeNums.push_back(i);
44         }
45     });
46 }
47 
48 void printInfo()
49 {
50     for_each (largeNums.begin(), largeNums.end(), [=](int i)
51     {
52         cout << i << " ";
53     });
54     cout << endl;
55 }
56 
57 void main()
58 {
59     initNums(); // 初始化值
60     filter(); // 过滤值
61     printInfo(); //打印信息
62 }

具体遇到其它的问题 ,再具体分析和学习。

【8】在返回类型明确的情况下,可以省略返回值类型,让编译器对返回类型进行推导

第一部分第4小节:“此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导”,示例如下:

 1 #include <iostream>
 2 #include <string>
 3 
 4 int main()
 5 {
 6     std::string str = "user_behavior_log";
 7     auto key = [&]() {
 8         if (str.empty())
 9         {
10             return std::string{};
11         }
12 
13         return (str + ".json");
14     };
15 
16     std::cout << key() << std::endl;
17 
18     return 0;
19 }
20 
21 // result
22 /*
23 user_behavior_log.json
24 */

希望能加深对返回类型的理解。

【9】lambda函数的总结

C++11中的Lambda表达式用于定义并创建匿名的函数对象,以简化编程工作。

posted @ 2015-01-18 15:36  kaizenly  阅读(9139)  评论(0编辑  收藏  举报
打赏