【C++学习】【lambda】

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》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值