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表达式用于定义并创建匿名的函数对象,以简化编程工作。