lambda函数

8 篇文章 0 订阅

在C++11中引入了lambda函数,焕然一新的感觉,从最早基于命令式编程范型的C语言,到加入了面向对象编程范型血统的C++,再到逐渐融入了函数式编程范型的lambda的新语言规范C++11。

lambda的基础应用

先来个简单的例子,求总人数和:

#include "stdafx.h"
#include <iostream>
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
    int girls = 3, boys = 4;
    auto totalChild = [](int x, int y)->int{ return x + y; };
    cout << totalChild(girls, boys) << endl;
    return 0;
}

上面的代码中,在第9行定义了一个lambda函数,该函数接受两个参数(int x,int y),并且返回一个int类型。通常情况下,lambda的语法规定如下:

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

其中,

  • [capture]:捕捉列表。捕捉列表总是出现在lambda函数的开始,也就是说,[]是lambda的引出符,编译器根据该引出符判断接下来的代码是不是lambda函数,捕捉列表能捕捉上下文中的变量以供lambda函数使用
  • (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以在连同()一起省略。
  • mutable:mutable修饰符。
  • ->return-type:返回类型。用追踪返回类型形式声明函数的返回类型。出于方便,不需要返回值的时候也可以连同符号->一起省略。此外,在返回值类型明确的情况下也可以省略该部分,让编译器对返回值类型进行推导。
  • {statement}:函数体。内容与普通函数一样,不过除了可以使用参数以外,还可以使用多有捕获的变量。

在lambda函数的定义中,参数列表和返回类型都是可选的部分,而捕捉列表和函数体可能为空。那么极端情况下,C++11中最为简略的lambda函数只需要声明为:

[]{};

不过上面的lambda函数显然只是个废材,什么事也没有做。

// 初识lambda.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>
#include <vector>
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
    /*int girls = 3, boys = 4;
    auto totalChild = [](int x, int y)->int{ return x + y; };
    cout << totalChild(girls, boys) << endl;*/
    []{};           //最简单的lambda函数
    int a = 3;
    int b = 4;
    [=]{ return a + b; };           //省略了参数列表和返回值类型,返回类型由编译器推断为int
    auto func1 = [&](int c){b = a + c; };   //省略了返回值类型,无返回值
    auto func2 = [=, &b](int c)->int{ return b += a + c; };     //各部分都很完整的lambda函数

    cout << "a=" << a << endl;
    cout << "b=" << b << endl;
    func1(5);
    cout << "a=" << a << endl;
    cout << "b=" << b << endl;
    func2(5);
    cout << "a=" << a << endl;
    cout << "b=" << b << endl;
    //cout << "c=" << c << endl;
    system("pause");
    return 0;
}

上面的代码中总共有4个lambda函数。有各种各样的捕捉列表,直观的说,lambda函数与普通函数可见的最大的区别之一,就是lambda函数可以通过捕捉列表访问一些上下文中的数据。具体而言,捕捉列表描述了上下文那些数据可以被lambda使用,以及是用什么方式使用的(以值传递的方式还是传引用的方式)。语法上,捕捉列表由多个捕捉项组成,并以逗号分割,如下:

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

然后就可以通过各种组合得到更为复杂的传递变量的方式,不过,捕捉列表不允许重复,会导致编译时的错误。通过上面的组合,可以进一步简化lambda函数:

    int girls = 3, boys = 4;
    /*auto totalChild = [](int x, int y)->int{ return x + y; };*/
    auto totalChild = [=]()->int{ return girls + boys; };
    cout << totalChild() << endl;

如上,第3行是对第2行的简化。但是须知道,依照C++11的标准,在块作用域(block scope,可以简单理解为在{}之内的任何代码都是块作用域的)以外的lambda函数捕捉列表必须为空。因此这样的lambda函数除去语法上面的不同以外,跟普通函数区别不大。而在块作用域中的lambda函数仅能捕捉父作用域的自动变量,捕捉任何非此作用域或者是非自动变量(如静态变量等)都会导致编译器报错:

#include <iostream>
#include <vector>
using namespace std;
static int cnt = 100;
auto func2 = [](int x, int y){return x + y; };
auto func3 = [=](int x)->int{return x*x; };
//auto gfunc4 = [&cnt](int x)->int{return x; };     //错误
auto gfunc5 = []()->int{ return cnt; };             //

编译器是不允许捕捉静态变量或者是相关的静态。

lambda与仿函数

那么lambda和仿函数有异同呢?下面我们来研究下……
仿函数简单地说,就是重新定义了成员函数operator()的一种自定义的对象,这样的对象有个特点,就是在其使用在代码层面跟函数的使用并无区别,但是究其本质,却并非函数:

#include <iostream>
#include <algorithm>
#include <iomanip>
using namespace std;
class test_functor{
public:
    int operator()(int x, int y)    { return x + y; }
};
int main()
{
    int girls = 3;
    int boys = 4;
    test_functor totalChild;
    cout << totalChild(girls, boys) << endl;
    system("pause");
    return 0;
}

这个例子中,class test_functor的operator()被重载,因此,在这里使用起来和函数并无明显区别。相比函数而言,仿函数可以拥有初始形态,一般通过class定义的私有成员,并在声明对象的时候对齐进行初始化,这样私有成员的状态就成了仿函数的初始状态。由于声明一个仿函数对象可以拥有多个不同的初始状态的实例,因此可以借由仿函数产生多个功能类似却不同的仿函数实例(这里是一个多状态的仿函数实例):

#include <iostream>
#include <algorithm>
#include <iomanip>
using namespace std;
class test_functor{
private:
    float rate;
    int base;
public:
    test_functor(float r, int b) :rate(r), base(b){}
    float operator()(float money){ return (money - base)*rate; }
};
int main()
{
    test_functor high(0.04, 3000);
    test_functor middle(0.24, 2000);
    cout << high(5000) << endl;
    cout << middle(3000) << endl;
    system("pause");
    return 0;
}

上面的例子下面我们比较一下仿函数和lambda函数:

#include <iostream>
#include <algorithm>
#include <iomanip>
using namespace std;
class test_functor{
private:
    float rate;
    int base;
public:
    test_functor(float r, int b) :rate(r), base(b){}
    float operator()(float money){ return (money - base)*rate; }
};
int main()
{
    float r = 0.40f;
    int b = 3000;
    test_functor high(0.04, 3000);
    test_functor middle(0.24, 2000);
    cout << high(5000) << endl;
    cout << middle(3000) << endl;
    auto test_lambda1 =
        [r, b](float money)->float{ return (money - b)*r; };
    cout << test_lambda1(5000) << endl;
    system("pause");
    return 0;
}

我们在25行加入了lambda函数。可以看到除去语法层面上的不同,lambda和仿函数有着相同的内涵,都可以捕捉一些变量作为初始状态,并接受参数进行运算。事实上,仿函数是编译器实现lambda的一种凡是。因此,在C++11中,lambda可以视为仿函数的一种等价形式。而且有些时候编译时发现lambda函数出现了错误,编译器会提示一些构造函数等相关的信息,这显示是由于lambda的这种实现方式照成的,通过STL的学习,可以知道仿函数经常被使用在STL中,。
lambda的基本使用
lambda的基础使用是很简单的呐,根据lambda的语法,就可以很轻松地完成lambda函数。也就是说,lambda函数本身的语法很简单,关键是在什么时候使用这种函数,首先可以想到的是用它来封装一些代码逻辑,使其不仅具有函数的封装性,同时又具有一定的自说明。其实我们从第一个例子就可以看到,在局部实现一个函数,并且该函数只能用于本函数或者固定的域内,不过lambda函数由于实现上的机制,其实在速度上是不能和inline函数相比的,但是。lambda函数的天生优势是不会产生对其他代码的污染,便于快速开发。来个例子:

int Prioritize(int);
int AllWorks(int times){
    int i;
    int x;
    try{
        for (i = 0; i < times; i++){
            x += Prioritize(i);
        }
    }
    catch (...){
        x = 0;
    }
    const int y = [=]{
        int i, val;
        try{
            for (i = 0; i < times; i++){
                val += Prioritize(i);
            }
        }
        catch (...){
            val = 0;
        }
        return val;
    }();
    return 0;
}

在上面的代码中,我们在函数AllWorks中初始化了两个变量,分别是x和y,而且二者的初始化过程是相同的,不同的是,x是变量而y是常量,这样就很有趣了,我们可以对于这中代码量不是很大的情况下,使用lambda函数而非定义个函数来对y进行初始化,更加清晰一点就是,用如此逻辑鲜明的lambda给常量y赋初值而非使用一个额外定义的函数,显得清晰和明了。
在上面的lambda的语法部分提及到捕捉列表,传递参数大体分为传引用和传值,这两种不同的使用有着略微不同的差别:

int _tmain(int argc, _TCHAR* argv[])
{
    int j = 12;
    auto by_val_lambda = [=]{ return j + 1; };
    auto by_ref_lambda = [&]{ return j + 1; };
    cout << "by_val_lambda: " << by_val_lambda() << endl;
    cout << "by_ref_lambda: " << by_ref_lambda() << endl;
    j++;
    cout << "by_val_lambda: " << by_val_lambda() << endl;
    cout << "by_ref_lambda: " << by_ref_lambda() << endl;
    system("pause");
    return 0;
}

先来结果:
运行结果
那么我们开始分析,并就此区分by_ref和by_val。不难推断出,对于by_val,其中的j为常量12,无论父作用域中j如何改变,都不会影响到其结果,我们可以认为在一初始化的时候,就已经将j的值固定在了那里,而对于by_ref来说,j仍在使用父作用域的,其实我们不妨认为并没有产生副本,还是原来的值。究其原因,从C++11的定义中,可以看到,lambda的类型被定义为“闭包”的类,而每个lambda表达式则会产生一个闭包类型的临时对象(右值),因此严格来讲,lambda并非函数指针。对于闭包(closure)的类,在C++11的标准中被定义为特有的(unique)、匿名且非联合体(unnamed nonunion)的class类型。不过C++11也并非不通情达理,lambda表达中如果没有捕捉任何变量,也可以向函数指针转换,且该函数指针所指函数原型必须跟lambda函数有着相同的调用方式:

// lambda函数_eg01.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
    int girls = 3;
    int boys = 4;
    auto totalChild = [](int x, int y)->int{return x + y; };
    typedef int(*allChild)(int x, int y);
    typedef int(*oneChild)(int x);
    allChild p;
    p = totalChild;
    oneChild q;
    //q = totalChild;               //这里会编译失败,提示指针参数必须一致
    decltype(totalChild) allPeople = totalChild;
    //decltype(totalChild) totalPeople = p;         //编译错误,指针无法转化为lambda
                                                    //提示不存在从allChild转换到lambda函数的适当的构造函数
    return 0;
}

在上面的代码中我们可以清晰地看到,没有捕捉列表的lambda函数可以转换为相同类型的函数指针,但是要求参数类型等要一致。decltype可以用来获得参数的类型,在这里是获得了lambda的类型,该用法还会在实例化模板的时候使用。我们在上面介绍lambda的语法构成的时候 还提及到了mutable,下面演示下如何使用:

// lambda函数mutable关键字.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>
#include <utility>
#include <algorithm>
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
    int val = 1;
    cout << val << endl;
    //编译失败,在const的lambda中修改常量
    //auto const_val_lambda = [=](){ val = 2; };        //编译错误,提示表达式必须是可以修改的左值
    //非const的lambda,可以修改常量数据
    auto mutable_val_lambda = [=]()mutable{ val = 3; };
    mutable_val_lambda();
    cout << val << endl;

    //依然是const的lambda,不过没有改动引用本身
    auto const_ref_lambda = [&](){ val = 4; };          //可以通过
    const_ref_lambda();
    cout << val << endl;
    //依然是const的lambda,通过参数传递val
    auto const_param_lambda = [](int &v){ v = 5; };
    const_param_lambda(val);
    cout << val << endl;
    system("pause");
    return 0;
}

除去第17行定义的lambda函数是错误的以外,其余的都可以编译通过。但是并不是都会改变val的值,运行结果如下:
运行结果
为什么会编译错误呢?在前面我们提过,在没有mutable说明的时候,lambda默认是const的,为了清晰一点的,不妨将lambda写成完整的仿函数,lambda函数的函数体部分在转换为完成的仿函数的时候,会成为一个class的常量成员函数:

class const_val_lambda{
public:
    const_val_lambda(int v) :_val(v){}
public:
    void operator()()const{
        _val = 3;           //这里也会产生编译错误
    }

private:
    int _val;
};

上面的代码是编译不能通过的,我们在const成员函数中试图修改class的成员变量,而这种行为在const成员函数中是不允许的。so,问题就很清楚了,lambda捕捉列表中的变量都会成为等价仿函数的成员变量,而常函数成员函数中修改其值是不允许的,所以按值捕捉的变量没有声明mutable的lambda函数中,不允许修改。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值