C/C++开发,C++ lambda技术点应用

目录

一、lambda概念

二、lambda的用法

三、lambda与函数指针

四、lambda与仿函数

  五、lambda应用优劣


一、lambda概念

        以lambda概念为基础的“函数式编程”(Functional Programming ) 是与命令式编程(Imperative Programming )、面向对象编程(Object-orientated Programming )等并列的一种编程范型(Programming Paradigm )。现在的高级语言也越来越多地引入lambda支持,很多近年流行的语言都提供了 lambda的支持,比如C#、PHP、JavaScript等。而C++自C++11其开始支持lambda,并在后续标准演进过程中不停地进行修正。C++从最早基于命令式编程范型的语言C,到加入了面向对象编程范型血统的C++,再到逐渐融入函数式编程范型的lambda的新语言规范C++11, C/C++的发展也在融入多范型支持的潮流中。

        c++在c++11标准中引入了lambda表达式,一般用于定义匿名函数,lambda表达式与普通函数类似,也有参数列表、返回值类型和函数体,只是它的定义方式更简洁,并且可以在函数内部定义。lambda的历时悠久,不过具体到C++11中,lambda函数却显得与之前C++规范下的代码在风格上有较大的区别。我们平时调用函数的时候,都是需要被调用函数的函数名,但是匿名函数就不需要函数名,而且直接写在需要调用的地方。

        C++11的基本语法格式为:
                [capture](parameters) mutable-> return_type { statement }
        (1) [capture] : 捕捉列表。捕捉列表总是出现在lambda函数的开始处,[]是 lambda引出符。编译器根据该引岀符判断接下来的代码是否是lambda函数。捕捉列表能够捕捉上下文中的变量以供lambda函数使用。[]内为外部变量的传递方式,值、引用等,如下:

  1. []        //表示的是在lambda定义之前的域,对外部参数的调用;
  2. [=]       //表示外部参数直接传值
  3. [&]       //表示外部参数传引用,可修改值。当默认捕获符是 & 时,后继的简单捕获符必须不以 & 开始。而当默认捕获符是 = 时,后继的简单捕获符必须以 & 开始。
  4. [x, &y]   //x is captured by value, y is captured by reference
  5. [&, x]    //x is explicitly captured by value. Other variables will be captured by reference
  6. [=, &z]   //z is explicitly captured by reference. Other variables will be captured by value

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

        (3)mutable:mutable修饰符。默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空)。
        (4)-> return_type:返回类型。用追踪返回类型形式声明函数的返回类型。->后面为lambda函数的返回类型,如 -> int、-> string等。一般情况下,编译器推出lambda函数的返回值,所以这部分可以省略不写。
        (5){ statement }:{}内为函数主体,和普通函数一样,不过除了可以使用参数之外,还可临, 用所有捕获的变量。

二、lambda的用法

        通过一个例子来了解lambda的用法:

#编译选项加上"-std=c++11"
int main() {
    int a= 2, b= 3;
    auto alls= [](int x,int y)->int{ return x + y; }; 
	printf("alls(a, b) = %d\n" , alls(a, b));
}

//编译运行输出alls(a, b) = 5

        在上面的例子当中,定义了一个lambda函数。该函数接受两个参数 (int x, int y),并且返回其和。直观地看,lambda函数跟普通函数相比不需要定义函数名,取而代之的多了一对方括号([])。此外,lambda函数还采用了追踪返回类型的方式声明其返回 值。其余方面看起来则跟普通函数定义一样。

        在lambda函数的定义中,参数列表和返还类型都是可选的部分,而捕捉列表和函数体 都可能为空。那么在极端情况下,C++11中最为简略的lambda函数只需要声明为
        []{};
        就可以了。不过该lambda函数不能做任何事情。

#编译选项加上"-std=c++11"
int main() {
   	[]{};	                    // 最简 lambda 函数
    int a = 3;
    int b = 4;
    [=]{ return a + b; };		//省略『参数列表与返回类型,返回类型由编译器推断为int
    auto fl = [&] (int c) { b = a + c; };		// 省略了返回类型,无返回值
    auto f2 = [=, &b](int c) ->int { return b += a + c; } ;	// 各部分都很完整的 lambda 函数
}

        从上面一些例子可以窥探lambda函 数与普通函数可见的最大区别之一,就是lambda函数可以通过捕捉列表访问一些上下文中 的数据。即捕捉列表描述了上下文中哪些的数据可以被lambda使用,以及使用方式 (以值传递的方式或引用传递的方式)。再看下面改写的例子:

#编译选项加上"-std=c++11"
int main() {
    int a= 2, b= 3;
	auto alls= [a, &b]()->int{return a+b; };
	printf("alls() = %d\n" , alls());
}

//编译运行输出alls() = 5

        在这个例子中,函数的原型发生了变化,即alls不再需要传递参数,此时a和b可以视为lambda函数的一种初始状态,lambda函数的运算则是基于初始状态进行的运算。这与函数简单基于参数的运 算是有所不同的。

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

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

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

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

  • [=, a]这里=已经以值传递方式捕捉了所有变量,捕捉a重复。
  • [&, &this]这里&已经以引用传递方式捕捉了所有变量,再捕捉this也是一种重复。

        通过上面的说明,再对上述例子进行变更:

#编译选项加上"-std=c++11"
int main() {
    int a= 2, b= 3;
	auto alls= [=]()->int{return a+b; };
	printf("alls() = %d\n" , alls());
}

//编译运行输出alls() = 5

        通过捕捉列表[=], lambda函数的父作用域中所有自动变量都被lambda依照传值的方式捕捉了。必须指出的是,依照现行C++11标准,在块作用域(block scope,可以简单理解为在 {}之内的任何代码都是块作用域的)以外的lambda函数捕捉列表必须为空。因此这样的 lambda函数除去语法上的不同以外,跟普通函数区别不大。而在块作用域中的lambda函数仅能捕捉父作用域中的自动变量,捕捉任何非此作用域或者是非自动变量(如静态变量等) 都会导致编译器报错。

三、lambda与函数指针

        lambda函数的类型以及该类型跟函数指针之间的关系,大多数情况下把匿名的lambda函数赋值给了一个auto类型的变量, 这是一种声明和使用lambda函数的方法。从C++11标准的定义上,lambda的类型被定义为“闭包"(closure)的类。每个lambda表达式则会产生一个闭包类型的临时对象(右值)。因此,严格地讲,lambda 函数并非函数指针。不过C++11标准却允许lambda表达是向函数指针的转换,但前提是 lambda函数没有捕捉任何变量,且函数指针所示的函数原型,必须跟lambda函数有着相同 的调用方式。再改写前面的例子看看:

#编译选项加上"-std=c++11"
int main() {
    int a= 3, b= 3;
	auto validate = [](int x, int y)->bool{ return x==y;};
    //auto validate = [=](int x, int y)->bool{ return x==y;};//转换函数指针会报错
	typedef bool (*equal)(int a, int b);
	equal p= validate;
	printf("equal(a,b) = %d\n" , (*p)(a,b) );
}

//编译运行输出equal(a,b) = 1

        函数指针方式以及lambda方式。函数指针的方式看似简洁)不过却有很大的缺陷。第一点是函数定义在别的地方,比如很多行以前(后)或者别的文件中, 这样的代码阅读起来并不方便。第二点则是出于效率考虑,使用函数指针很可能导致编译 器不对其进行inline优化(inline对编译器而言并非强制),在循环次数较多的时候,内联的lambda和没有能够内联的函数指针可能存在着巨大的性能差别。因此,相比于函数指针, lambda还是拥有不少独特的优势。

四、lambda与仿函数

        在C++11之前,我们在使用STL算法时,通常会使用到一种特别的对象,一般来说,我们称之为函数对象,或者仿函数 (functor )o仿函数简单地说,就是重定义了成员函数operator ()的一种自定义类型对象。例如:

#编译选项加上"-std=c++11"
class _functor {
	public:
	int operator()(int x, int y) { return x + y; };
};

int main() (
    int a= 2, b= 3;
	_functor alls;
	printf("alls(a,b) = %d\n" , alls(a,b) );
}

//编译运行输出alls(a,b) = 5

        上述例子中,class _functor的operator()被重载,因此,在调用该函数的时候,我们看到跟函数调用一样的形式,只不过这里的totalChild不是函数名称,而是对象名称。可以看到lambda和仿函数却有着相同的内涵一一都可以捕捉一些变量作为初始状态, 并接受参数进行运算。事实上,仿函数是编译器实现lambda的一种方式。在现阶段,通常编译器都会把 lambda函数转化为成为一个仿函数对象。因此,在C++11中,lambda可以视为仿函数的一种等价形式了。

        一直以来,仿函数被广泛地用于STL中,同样的,但在C++11中,lambda也在标准库中被广泛地使用。由于其书写简单,通常可以就地定义,因此,常可以使用lambda代替仿函数来实现相同功能代码。

        一个最为常见的STL算法for_each,for_each的一个示意实现如下:

for_each(iterator begin,iterator end,Function fn)
{
  for(iterator i = begin;i != end;i++)
    fn(*i);
}

        上面描述总,for_each算法需要一个标记开始的iterator,一个标记结束的iterator,以及一个接受单个参数的“函数”(即一个函数指针、仿函数或者lambda 函数),可以直观地看到lambda函数比仿函数书写上的简便性,从代码的简洁性上来看,lambda的出现使得学习使用STL算法的代价大大降低。

  五、lambda应用优劣

        虽然lambda比仿函数能有效降低算法的复杂性,但是在现有C++11中,lambda并不是仿函数的完全替代者。这一点很 大程度上是由lambda的捕捉列表的限制造成的。在现行C++11标准中,捕捉列表仅能捕捉父作用域的自动变量,而对超出这个范围的变量,是不能被捕捉的。

int a = 0 ;
void myCapture() {
	auto my_lambda = [a]{};
};

        上面语句虽然能编译通过,但是会出现告警,一些严格的C++编译器甚至会报错。

         采用仿函数实现类似功能,就不会有这样的编译限制,即仿函数可以被定义以后在不同的作用域范围内取得初始值。这使得仿函数天生具有了跨作用域共享的特征

int a = 0;
class _Functor{
public:
	_Functor(int a): val(a){};
	void operator () () const {};
private:
	int val;
};
void myCapture()
{
	_Functor _fun(a);
};

        lambda函数被设计的目的,就是要就地书写,就地使用。使用lambda的用 户,更倾向于在一个屏幕里看到所有的代码,而不是依靠代码浏览跳转在文件间找到函数的实现。而在封装的思维层面上,lambda 应是一种局部的封装,以及局部的共享。而需要全局共享的代码逻辑,我们则还是需要用函数无状态)或者仿函数(有状态)封装起来。

        虽然lambda的语法看起来不像是“典型的C++”的,但一旦熟悉了之后,程序员就能准确地完成一个简单的、就地的、带状态的函数定义。虽然lambda可以跟仿函数的概念一一对应也实际由仿函数来实现,但理解lambda显然比仿函数更加容易。
        此外,lambda作为局部函数也会使得复杂代码的开发加速。通过lambda函数,程序员可以轻松地在函数内重用代码,而无需费心设计接口。事实上,lambda函数的出现也导致了函数的作用域在 C++11中再次被细分,从而也使得C++编程具备了更多的可能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

py_free-物联智能

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值