C++11新标准之Lambda表达式

写在前面

同std::function函数包装器一样,工作中用到的Lambda表达式,正好搭配std::function 使用,简洁高效。

关于std::function 的使用,可参考:C++11新标准之std::function函数包装器

lambda表达式是在C++11及之后版本中支持的新的语法特性,lambda函数、匿名函数、闭包等说的都是lambda表达式,lambda表达式常用在 调用或作为函数参数传递时。

Lambda表达式语法

先看一个简单的lambda表达式示例:

auto func = [](){
    cout << "hello world!\n";
};

func();

上述代码声明了一个简单的lambda表达式,用一个自动类型变量func接收,这里的func就相当于一个函数指针,指向lambda函数,即lambda表达式。

关于auto自动类型,可参考:C++11新标准之auto类型说明符

上面的lambda表达式省略了部分语法,其完整语法如下:

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

[捕获列表] (参数列表) 可变规范 异常规范 ->返回类型 { lambda主体,即函数体 }
  1. 捕获列表(capture list): 在C++规范中也称为Lambda导入器,捕获列表必须出现在lambda表达式的开始处。实际上,[]是Lambda表达式的引出符,编译器根据该引出符判断接下来的代码是否是Lambda表达式,捕获列表能够捕获Lambda表达式所属上下文中的变量以供Lambda表达式使用,因此一个Lambda表达式中捕获列表[]不可省略
  2. 参数列表(parameters):与普通函数的参数列表一致。如果不需要传递参数,则可省略()。
  3. 可变规范(mutable): mutable修饰符。默认情况下Lambda表达式是一个const函数,mutable可以取消其常亮性,可省略。注意:在使用mutable修饰符时,不可省略参数列表(即使参数列表为空)。
  4. 异常规范( throw() ): 用于Lambda表达式内部抛出异常,可省略
  5. 返回类型(->return type): 类似普通函数的返回类型部分,但Lambda表达式的返回类型必须在->后。在不需要返回类型的时候可以省略该部分(连同->一起省略)。此外,在返回类型明确的时候,也可省略该部分,由编译器自动推导返回类型。
  6. Lambda主体(state): 类似普通函数的函数体,区别是Lambda主体除了可以使用参数外,还能使用所有捕获列表中捕获的变量,不可省略Lambda主体。

捕获列表(不可省略)

Lambda表达式与普通函数最大的区别是,除了可以使用参数以外,Lambda函数还可以通过捕获列表访问一些上下文中的数据。

具体地,捕捉列表描述了上下文中哪些数据可以被Lambda使用,以及使用方式(以值传递的方式或引用传递的方式)。

语法上,在“[]”包括起来的是捕获列表,捕获列表由多个捕获项组成,并以逗号分隔。捕获列表有以下几种形式:

不捕获

[]捕获列表中无任何值,则表示不捕获任何变量。

int a = 5;
int b = 10;
auto func = [](){
    cout << "hello world!\n";
};

auto func2 = [](){
    cout << "a + b = " << a + b << endl;  //错误,lambda主体中使用未捕获的a,b变量
};

func();
func2();

值捕获

值捕获有以下几种形式:

[变量1, 变量2,…] 显示指定以值传递方式捕获列表中变量。

int a = 5;
int b = 10;
//显示捕获变量
auto func = [a, b]{
    cout << "a + b = " << a + b << endl;
};

func();

需要知道的是,通过值捕获的方式,在Lambda表达式主体中修改捕获变量,不会影响原值:

//因为Lambda表达式默认是const的,因此这里添加mutable声明, 且参数列表不可省略,mutable将在下面详细介绍
auto func = [a, b]() mutable {	
    cout << "a + b = " << (a++) + (b++) << endl;
};

func();		//a + b = 15
cout << "a: " << a << ", b: " << b << endl; //a: 5, b: 10

[=]表示以值传递方式捕获所有父作用域的变量(包括this) 。

int a = 5;
int b = 10;
auto func = [=]{
    cout << "a + b = " << a + b << endl;
};

func();

捕获this示例:

class Tmp
{
public:
    Tmp() : m_nVal(5) 
    {
        
    }

    void output()
    {
        cout << "Tmp::output \n";
    }

    void lambdaTmp()
    {
        //这里没有参数列表,可省略()
        //=捕获父作用域(即lambdaTmp函数体内)中所有变量(包括this)
        [=]{
            output();
            cout << "Tmp::m_nVal: " << m_nVal << endl;
        }();	//这里不再借助自动类型变量func,直接调用
    }
    
    //需要注意这种情况
    void lambdaTmp2()
    {
        [=]()mutable{
            //这里输出: lambdaTmp2 Lambda主体中m_nVal: 5
            cout << "lambdaTmp2 Lambda主体中m_nVal: " << m_nVal++ << endl;
        }();
        
        
        //以值传递方式传递this,改变指针不会影响原指针,但改变指针指向内存的值会影响原内存值
        //这里输出:lambdaTmp2中m_nVal: 6
        cout << "lambdaTmp2中m_nVal: " << m_nVal << endl;
    }

private:
    int m_nVal;
};

Tmp t;
t.lambdaTmp();
t.lambdaTmp2();

因上面不允许改变this指针值,因此这里再单独使用局部指针变量示例:

int a = 5;
int* pInt = &a;
//值传递方式
[=]()mutable{
    //这里输出后,pInt++改变指针指向(危险行为),改变指针指向内存值(即变量a的值)
    cout << "pInt: " << pInt++ << ", *pInt: " << (*pInt)++ << endl;
}();

//因为是值传递,因此指针指向没有改变,但指针指向的内存值有改变
cout << "pInt: " << pInt << ", *pInt: " << *pInt << ", a: " << a << endl;

//输出如下:
//pInt: 0053F90C, *pInt: 5
//pInt: 0053F90C, *pInt: 6, a: 6

引用捕获

[&变量]表示以引用传递方式捕捉变量:

注意: 引用会破坏Lambda表达式默认的const性质。

int a = 5;
int b = 10;
auto func = [&a, &b](){	//这里不用mutable修饰,也能修改原值。即引用破坏了Lambda表达式的const性质
    cout << "a + b = " << (a++) + (b++) << endl;
};
func();	//a + b = 15
cout << "a: " << a << " ,b: " << b << endl;	//输出 a: 6 ,b: 11

[&]表示以引用传递方式捕捉所有父作用域的变量(包括this) 。

int* pInt = &a;
[&]()mutable{
    cout << "pInt: " << pInt++ << ", *pInt: " << (*pInt)++ << endl;
}();
cout << "pInt: " << pInt << ", *pInt: " << *pInt << ", a: " << a << endl;

//输出如下:
//pInt: 012FF96C, *pInt: 5
//pInt: 012FF970, *pInt: -858993460, a: 6	//可以看到改变指针指向了

获取this指针的引用,因为this是一个const 指针,因此不允许改变this的值,此外**不论是以值方式还是引用方式传递this,传递的都是this指针的值。**如下。

this捕获

this较为特殊,因此这里单独介绍下。

可以看到上面可以通过[=] 和 [&]捕获到this,但也可以显示的只捕获this:

class Tmp
{
    public:
    Tmp() : m_nVal(5) 
    {

    }

    void output()
    {
        cout << "Tmp::output \n";
    }

    void lambdaTmp()
    {
        
        //[&this]{ 这里编译会报错:error C3496: “this”始终按值捕获: 已忽略“&”
        [this]{
            output();
            cout << "Tmp::m_nVal: " << m_nVal << endl;
        }();
    }

    private:
    int m_nVal;
};

Tmp t;
t.lambdaTmp(); 

//输出
//Tmp::output
//Tmp::m_nVal: 5

可以知道,无论是[=] 和 [&]隐式捕获,或者[this]显示捕获this指针时,均是以值传递方式捕获,因为this是一个const 指针,而不存在传递this指针的引用。

组合捕获

捕获列表中可以同时出现值捕获和引用捕获,例:

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

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

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

如果在Lambda主体中的total变量是通过引用访问的外部变量,并factor变量是通过值访问外部变量,则以下捕获子句是等效的:

[&total, factor]
[factor, &total]
[&, factor]
[factor, &]
[=, &total]
[&total, =]

参数列表(可选)

Lambda表达式的参数列表类似于普通函数的参数列表,当然这里对Lambda表达式的参数列表会有以下限制

  1. 参数列表中不能有默认参数
  2. 参数列表中不能有一个可变长度参数列表
  3. 参数列表中不能有未命名的参数

参数列表中不能有默认参数:

auto func = [](int val = 5){	//错误,参数不能有默认值
	cout << "val: " << val << endl;
};

参数列表中不能有一个可变长度参数列表,但实际使用可变参数列表时,编译竟然也没有报错:

   auto func = [](int a, ...) {
       int* p = &a + 1;
       while (a--)
       {
           cout << *p++ << endl;
       }
   };

   func(5, 1, 2, 3, 4, 5);

输出如下:
1

第三点参数列表中不能有未命名参数也未报错:

    auto func = [](int a, int) {
        cout << "a: " << a << endl;
    };
    func(5, 4);

百思不得其解。。。
这里暂且刷锅MSDN帮助文档未更新吧。。。

Lambda表达式可以作为另一个Lambda表达式的参数传递。

auto func = [](const function<void (int)>& f, int val){
    f(val);
};

func([](int val){cout << val << endl;}, 5);	//输出:5

Lambda表达式的参数列表是可选的,当没有参数需要传递时可省略,同时也可省略其后的可变规范、异常规范以及返回类型。如下:

auto func = []{	//省略参数列表、可变规范、异常规范及返回类型
    cout << "hello world! \n";
};

func();

这里可以看到,Lambda表达式必须包括 捕获列表和Lambda主体,其他都是可选项。

此外,参数列表中参数会覆盖同盟的捕获变量和全局静态变量:

int g_nVal = 10;
{
	int a = 1;
	int b = 2;
    auto func = [&](int a, int b, int g_nVal) {
        cout << "a: " << a << ", b: " << b << ", g_nVal: " << g_nVal << endl;
    };

    func(0, 0, 5);	//输出: a: 0, b: 0, g_nVal: 5
}

可变规范(可选)

Lambda表达式默认是一个const函数,不能修改通过 值传递 方式捕获的相关变量。

mutable修饰符会破坏其通过 值传递 方式的const特性。

此外,通过 引用传递 的方式无需mutable,也能破坏其const特性。

注意:当指定mutable时,不可省略参数列表。

auto func = [a, b]()mutable {	//指定mutable时不可省略参数列表
    a++;
    b++;
};

func();

异常规范(可选)

你可以使用 **throw()**异常规范来指示 Lambda 表达式不会引发任何异常。

与普通函数一样,如果 Lambda 表达式声明 C4297 异常规范且 Lambda 体引发异常,Visual C++ 编译器将生成警告 throw()

注意:使用异常规范,不可省略参数列表。

   try
   {
       auto func = []()throw() {
           throw 5;
       };
       func();
   }
   catch (...)
   {
       cout << "catch any exception \n";
   }

在MSDN的异常规范中,明确指出异常规范是在 C++11 中弃用的 C++ 语言功能。因此这里不建议使用。

返回类型(可选)

Lambda表达式的返回类型会自动推导。除非你指定了返回类型,否则不必使用关键字。

返回型类似于通常的方法或函数的返回型部分。但是,返回类型必须在参数列表之后,并且返回类型需在 -> 之后。

如果Lambda主体仅包含一个return语句或该表达式未返回值,则可以省略Lambda表达式的return-type部分。

如果Lambda主体包含一个return语句,则编译器将从return表达式的类型中推断出return类型。否则,编译器将返回类型推导为void

auto ret = [](){return 5;}();	//自动推导返回类型, 可省略return-type

Lambda主体(不可省略)

Lambda表达式的Lambda主体可以包含 普通方法或函数的主体可以包含的任何内容。Lambda表达式的主体可以访问以下类型的变量:

  • 捕获变量
  • 形参变量
  • 局部声明的变量
  • 类数据成员,当在类内声明this并被捕获时
  • 具有静态存储持续时间的任何变量,例如全局变量

前四点已在之前示例中体现,因此这里仅考虑全局变量的情况

//全局变量
int g_nVal = 10;

{
    auto func = [](){
    cout << "access global variable in lambda: " << g_nVal << endl;
    };
    
    func();
}

Lambda表达式工作原理

编译器会把一个Lambda表达式生成一个匿名类匿名对象,并在类中重载函数调用运算符,实现了一个operator()方法。

例定义使用一个Lambda表达如下:

auto func = []{
    cout << "hellor world! \n";
};

相当于编译器自动生成这样一个匿名类:

class func
{
public:
    void operator()(void) const
    {
        cout << "hellor world! \n";
    }
};

可以看到这是一个仿函数(或称为函数对象)

总结

Lambda表达式以捕获列表[]开头,且必须包含Lambda主体,其他项可按需省略。

关于捕获列表,需要注意的是 按值传递 的捕获的Lambda表达式是const的,无法更改其捕获的变量,可通过 引用传递 方式或mutable修饰符破坏其const特性。

关于参数列表,需注意与普通函数参数列表的区别,同时无参数时可省略。

关于可变规范,可通过mutable破坏以 值传递 捕获的Lambda表达式,注意使用mutable时不可省略参数列表。

关于异常规范,一般不建议使用,但使用时不能省略参数列表。

关于返回类型,需在 -> 之后,可省略。

关于Lambda主体,不可省略且必须。

问题

既然有了捕获列表,为什么还要参数列表呢?即捕获列表和参数列表有什么区别?

前面首先提到Lambda表达式必须以捕获列表[]开头,这是编译器区分Lambda表达式和普通函数的依据, 因此捕获列表是必须的。

这里再从Lambda表达式的原理方面来研究下,上面提到Lambda表达式的原理就是在定义一个Lambda表达式时,编译器会自动为其生成一个匿名类,并声明一个改匿名类的对象,改匿名类中重载了函数调用运算符(), 以使其能够通过对象调用。

既然作为类,那么肯定是有构造函数的,假如在定义Lambda表达式时在捕获列表中显示的指定了变量,那么匿名类应该会将这些变量声明成成员变量,以支持在operator()中使用,如下:

int a = 1;
int b = 2;
{
    auto func = [a, b]() {
        cout << "a: " << a << ", b: " << b << endl;
    };

    func();
}

//编译器会这样处理
class Tmp
{
public:
	Tmp() : a(1), b(2)
	{
		
	}

	void operator()() const	//因为是值传递,因此函数调用重载是const的
	{
		cout << "a: " << a << ", b: " << b << endl;
	}

private:
	int a;
	int b;
};

因此这里可以这样认为:没有捕获列表的可调用对象就像是一个普通函数,而具有捕获列表的可调用对象就像一个具有适当定义和可初始化私有成员的仿函数对象。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值