1、前言
之前接触mcu开发的时候使用的语言都是C语言,没有接触过Lambda函数;但是做soc的项目,使用了C++语言,在项目中有大量的lambda函数,首先要求能看得懂,其次,在以后的项目功能开发中,要会使用Lambda函数实现相应的功能。
2、什么是Lambda表达式
Lambda 表达式(lambda expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包,和传统数学上的意义有区别。这是官方对Lambda表达式的定义。
来看一个最简单的Lambda函数,依然是经典的“Hello world”程序。
#include<iostream>
using namespace std;
int main(void){
auto basiclambda = []{cout << "hello, world" << endl;};
basiclambda();
return 0;
}
3、为什么使用Lambda表达式
Lambda表达式的作用,可以由函数指针,函数符进行替代;最后表现的结果,实现的功能是一样的。我们举一个整除的例子,来做一下比较。
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>
#include <algorithm>
using namespace std;
#define SIZE1 39
#define SIZE2 3900
#define SIZE3 390000
bool f3(int x){return x % 3 == 0;}
bool f13(int x){return x % 13 == 0;}
int main(void)
{
vector<int> numbers(SIZE1);
//method1
srand(time(0));
generate(numbers.begin(), numbers.end(), rand);
cout << "Sample size = " << SIZE1 << endl;
int count3 = count_if(numbers.begin(), numbers.end(), f3);
cout << "Count of numbers divisible by 3: " << count3 << endl;
int count13 = count_if(numbers.begin(), numbers.end(), f13);
cout << "Count of numbers divisible by 13: " << count13 << endl;
//method 2
numbers.resize(SIZE2);
generate(numbers.begin(), numbers.end(), rand);
cout << "Sample size = " << SIZE2 << endl;
class f_mod
{
private:
int dv;
public:
f_mod(int d = 1) : dv(d){}
bool operator()(int x){return x % dv == 0;}
};
count3 = count_if(numbers.begin(), numbers.end(), f_mod(3));
cout << "Count of numbers divisible by 3: " << count3 << endl;
count13 = count_if(numbers.begin(), numbers.end(), f_mod(13));
cout << "Count of numbers divisible by 13: " << count13 << endl;
//method 3
numbers.resize(SIZE3);
generate(numbers.begin(), numbers.end(), rand);
cout << "Sample size = " << SIZE3 << endl;
count3 = count_if(numbers.begin(), numbers.end(), [](int x){return x % 3 == 0;});
cout << "Count of numbers divisible by 3: " << count3 << endl;
count13 = count_if(numbers.begin(), numbers.end(), [](int x){return x % 13 == 0;});
cout << "Count of numbers divisible by 13: " << count13 << endl;
return 0;
}
这个程序例子来自于《C++ Primer Plus》。
首先看一下method1,他定义了f3,f13这两个函数,定义完上述函数之后,便可以计算符合条件的元素数了,这是方法一,比较常用,有明显的函数调用痕迹,在C,C++中都是比较常见的。
再看一下method2,他是用函数符来完成这个任务的,主要是定义的类方法operator()(),可以使用构造函数创建存储特定数值的f_mod对象。
最后看method3,[](int x){return x % 3 == 0;}
,这是一个匿名函数的形式,这个表达式和bool f3(int x){return x % 3 == 0;}
相似度很高,少了函数返回值类型以及函数名称。
总结:
从上面的例子,做一个比较,从4个方面来讨论为什么使用Lambda函数:距离,简洁,效率和功能。
(1)距离
所谓的距离,就是函数的定义以及函数使用的距离。这是出于理解源代码的角度,Lambda函数是个很好的选择,他的定义和使用是在同一个地方进行的;函数符也可以,在函数内部定义类,函数定义距离函数使用很近;函数是比较糟糕的选择,通常情况下,函数的定义与函数的使用存在着一定的距离。
(2)简洁
从代码简洁的角度来看,函数符是最繁琐的,函数运算和Lambda运算差不多。但是函数运算的优势是只要写一个f3,f13,以后再用到f3,f13,便可以复用。但Lambda也有这样的功能。
auto mod3 = []{return x%3 == 0;}
这个时候就给Lambda指定了一个名称,同样做到了复用。
(3)效率
三个方法的相对效率取决于编译器内联的相关资源。在编译器传统上不会内联其他地址被获取的函数,函数地址的概念意味着非内联函数;而函数运算符和Lambda运算符不会阻止内联。
(4)功能
功能上,Lambda会更加强大一些,Lmabda可以访问作用域内的任何动态变量。
示例代码:
int main(void)
{
vector<int> numbers(SIZE);
int count3, count13;
count3 = count13 = 0;
//method 3
numbers.resize(SIZE);
generate(numbers.begin(), numbers.end(), rand);
cout << "Sample size = " << SIZE << endl;
count3 = count_if(numbers.begin(), numbers.end(), [](int x){return x % 3 == 0;});
cout << "Count of numbers divisible by 3: " << count3 << endl;
for_each(numbers.begin(), numbers.end(), [&count13](int x){count13 += x % 13 == 0;});
cout << "Count of numbers divisible by 13: " << count13 << endl;
count3 = count13 = 0;
for_each(numbers.begin(), numbers.end(), [&](int x){count3 += x % 3 == 0; count13 += x % 13 == 0;});
cout << "Count of numbers divisible by 3: " << count3 << endl;
cout << "Count of numbers divisible by 13: " << count13 << endl;
return 0;
}
[&]能够按引用访问所有动态的变量,如何使用下面会做详细的介绍。
4、Lambda表达式的语法
Lambda表达式如下:
// 完整语法
[ capture-list ] ( params ) mutable(optional) constexpr(optional)(c++17) exception attribute -> ret { body }
// 可选的简化语法
[ capture-list ] ( params ) -> ret { body }
[ capture-list ] ( params ) { body }
[ capture-list ] { body }
第一个是完整的语法,后面3个是可选的语法。这意味着lambda表达式相当灵活,但是照样有一定的限制,比如你使用了拖尾返回类型,那么就不能省略参数列表,尽管其可能是空的。针对完整的语法,我们对各个部分做一个说明:
- capture-list:捕捉列表,这个不用多说,前面已经讲过,记住它不能省略;
- params:参数列表,可以省略(但是后面必须紧跟函数体);
- mutable:可选,将lambda表达式标记为mutable后,函数体就可以修改传值方式捕获的变量;
- constexpr:可选,C++17,可以指定lambda表达式是一个常量函数;
- exception:可选,指定lambda表达式可以抛出的异常;
- attribute:可选,指定lambda表达式的特性;
- ret:可选,返回值类型;
- body:函数执行体。
举几个Lambda的例子:
[](int x, int y) { return x + y; } // 隐式返回类型
[](int& x) { ++x; } // 没有return语句 -> lambda 函数的返回类型是'void'
[]() { ++global_x; } // 没有参数,仅访问某个全局变量
[]{ ++global_x; } // 与上一个相同,省略了()
从列举的几个例子可以看出,Lambda函数,不一定带有参数,也不一定一定要有返回类型;但是他的capture[]是不能省略的。
5、Lambda的capture[]
Lambda捕获是lambda比较重要的一个知识点,捕获的方式可以是引用也可以是复制,但是具体说来会有以下几种情况来捕获其所在作用域中的变量:
- *[]:默认不捕获任何变量;
auto function = ([]{
std::cout << "Hello World!" << std::endl;
}
);
function();
- [=]:默认以值捕获所有变量;
int index = 1;
int num = 100;
auto function = ([=]{
std::cout << "index: "<< index << ", "
<< "num: "<< num << std::endl;
}
);
function();
- [&]:默认以引用捕获所有变量;
int index = 1;
int num = 100;
auto function = ([&]{
num = 1000;
index = 2;
std::cout << "index: "<< index << ", "
<< "num: "<< num << std::endl;
}
);
function();
- [x]:仅以值捕获x,其它变量不捕获;
int num = 100;
auto function = ([num]{
std::cout << num << std::endl;
}
);
function();
- [&x]:仅以引用捕获x,其它变量不捕获;
int num = 100;
auto function = ([&num]{
num = 1000;
std::cout << "num: " << num << std::endl;
}
);
function();
- [=, &x]:默认以值捕获所有变量,但是x是例外,通过引用捕获;
int index = 1;
int num = 100;
auto function = ([=, &index, &num]{
num = 1000;
index = 2;
std::cout << "index: "<< index << ", "
<< "num: "<< num << std::endl;
}
);
function();
- [&, x]:默认以引用捕获所有变量,但是x是例外,通过值捕获;
int index = 1;
int num = 100;
auto function = ([&, num]{
//num = 1000;
index = 2;
std::cout << "index: "<< index << ", "
<< "num: "<< num << std::endl;
}
);
function();
- [this]:通过引用捕获当前对象(其实是复制指针);
#include <iostream>
using namespace std;
class Lambda
{
public:
void sayHello() {
std::cout << "Hello" << std::endl;
};
void lambda() {
auto function = [this]{
this->sayHello();
};
function();
}
};
int main()
{
Lambda demo;
demo.lambda();
}
- [*this]:通过传值方式捕获当前对象;
#include <iostream>
using namespace std;
class Lambda
{
public:
void sayHello() {
std::cout << "Hello" << std::endl;
};
void lambda() {
auto function = [*this]{
this->sayHello();
};
function();
}
};
int main()
{
Lambda demo;
demo.lambda();
}
5.1谨慎使用**[=]和[&]**
在上面的捕获方式中,注意最好不要使用**[=]和[&]默认捕获所有变量。首先说默认引用捕获所有变量,你有很大可能会出现悬挂引用(Dangling references)**,因为引用捕获不会延长引用的变量的声明周期:
std::function<int(int)> add_x(int x)
{
return [&](int a) { return x + a; };
}
因为参数x仅是一个临时变量,函数调用后就被销毁,但是返回的lambda表达式却引用了该变量,但调用这个表达式时,引用的是一个垃圾值,所以会产生没有意义的结果。你可能会想,可以通过传值的方式来解决上面的问题:
std::function<int(int)> add_x(int x)
{
return [=](int a) { return x + a; };
}
是的,使用默认传值方式可以避免悬挂引用问题。但是采用默认值捕获所有变量仍然有风险,看下面的例子:
class Filter
{
public:
Filter(int divisorVal):
divisor{divisorVal}
{}
std::function<bool(int)> getFilter()
{
return [=](int value) {return value % divisor == 0; };
}
private:
int divisor;
};
这个类中有一个成员方法,可以返回一个lambda表达式,这个表达式使用了类的数据成员divisor。而且采用默认值方式捕捉所有变量。你可能认为这个lambda表达式也捕捉了divisor的一份副本,但是实际上大错特错。问题出现在哪里呢?因为数据成员divisor对lambda表达式并不可见,你可以用下面的代码验证:
// 类的方法,下面无法编译,因为divisor并不在lambda捕捉的范围
std::function<bool(int)> getFilter()
{
return [divisor](int value) {return value % divisor == 0; };
}
那么原来的代码为什么能够捕捉到呢?仔细想想,原来每个非静态方法都有一个this指针变量,利用this指针,你可以接近任何成员变量,所以lambda表达式实际上捕捉的是this指针的副本,所以原来的代码等价于:
std::function<bool(int)> getFilter()
{
return [this](int value) {return value % this->divisor == 0; };
}
尽管还是以值方式捕获,但是捕获的是指针,其实相当于以引用的方式捕获了当前类对象,所以lambda表达式的闭包与一个类对象绑定在一起了,这也很危险,因为你仍然有可能在类对象析构后使用这个lambda表达式,那么类似“悬挂引用”的问题也会产生。所以,采用默认值捕捉所有变量仍然是不安全的,主要是由于指针变量的复制,实际上还是按引用传值。
通过前面的例子,你还可以看到lambda表达式可以作为返回值。我们知道lambda表达式可以赋值给对应类型的函数指针。但是使用函数指针貌似并不是那么方便。所以STL定义在头文件提供了一个多态的函数对象封装std::function,其类似于函数指针。它可以绑定任何类函数对象,只要参数与返回类型相同。如下面的返回一个bool且接收两个int的函数包装器:
std::function<bool(int, int)> wrapper = [](int x, int y) { return x < y; };
5.2按值捕获与按引用捕获的区别
#include <iostream>
using namespace std;
int main()
{
int a = 5;
auto f1 = [=]{return a+1;};//按值捕获a
auto f2 = [&]{return a+1;};//按引用捕获a
cout << f1() << endl;
cout << f2()<< endl;
a++;
cout << f1() << endl;
cout << f2() << endl;
return 0;
}
程序运行结果是
6
6
6
7
前2个6是意料之中的,但是做了a++的操作后,按值捕获依然是6,按引用捕获确是7;这就是按值捕获与按引用捕获的工作机制不一样导致的:按值捕获,相当于在表达式内生成了一个被捕获变量的副本,lambda表达式就一直使用的这个副本,原有变量再怎样变化都不会影响lambda表达式输出的数值,而按引用捕获,在调用访问外部的变量时,所得到的变量是变量本身不是副本,所有在使用中要注意是要是有这个数值的副本,还是使用变量本身。
6、Lambda用法示例
6.1普通用法
#include<iostream>
using namespace std;
int main(void){
auto basiclambda = []{cout << "hello, world" << endl;};
basiclambda();
return 0;
}
6.2进阶用法
待更新
7、Lambda表达式的优缺点
优点:
优点在之前讲为什么使用lamnda表达式的时候提到过;除此之外还有以下优点。
- 非常容易并行计算;
- 可能代表未来的编程趋势;
缺点:
- 若不用并行计算,很多时候计算速度没有比传统的 for 循环快。(并行计算有时需要预热才显示出效率优势)
- 不容易调试。
- 若其他程序员没有学过 lambda 表达式,代码不容易让其他语言的程序员看懂。
8、参考连接:
1、https://www.cnblogs.com/HDK2016/p/11183341.html#a3
2、http://t.csdnimg.cn/Dtm5z
3、https://www.cnblogs.com/pzhfei/archive/2013/01/14/lambda_expression.html
4、《C++ Primer Plus》