一文读懂c++11 Lambda表达式

所属分类: 软件编程 / C 语言 阅读数: 149
收藏 0 赞 0 分享

1.简介

1.1定义

C++11新增了很多特性,Lambda表达式(Lambda expression)就是其中之一,很多语言都提供了 Lambda 表达式,如 Python,Java ,C#等。本质上, Lambda 表达式是一个可调用的代码单元[1]^{[1]}[1]。实际上是一个闭包(closure),类似于一个匿名函数,拥有捕获所在作用域中变量的能力,能够将函数做为对象一样使用,通常用来实现回调函数、代理等功能。Lambda表达式是函数式编程的基础,C++11引入了Lambda则弥补了C++在函数式编程方面的空缺。

1.2作用

以往C++需要传入一个函数的时候,必须事先进行声明,视情况可以声明为一个普通函数然后传入函数指针,或者声明一个仿函数(functor,函数对象),然后传入一个对象。比如C++的STL中很多算法函数模板需要传入谓词(predicate)来作为判断条件,如排序算法sort。谓词就是一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法所使用的谓词分为两类:一元谓词(unary predicate,只接受单一参数)和二元谓词(binary predicate,接受两个参数)。接受谓词的算法对输入序列中的元素调用谓词,因此元素类型必须能转换为谓词的参数类型。如下面使用sort()传入比较函数shorter()(这里的比较函数shorter()就是谓词)将字符串按长度由短至长排列。

//谓词:比较函数,用来按长度排列字符串
bool shorter(const string& s1,const string& s2)
{
 return s1.size()<s2.size();
}

//按长度由短至长排列words
std::sort(words.begin(),words.end(),shorter);

Lambda表达式可以像函数指针、仿函数一样,作为一个可调用对象(callable object)被使用,比如作为谓词传入标准库算法。

也许有人会问,有了函数指针、函数对象为何还要引入Lambda呢?函数对象能维护状态,但语法开销大,而函数指针语法开销小,却没法保存函数体内的状态。如果你觉得鱼和熊掌不可兼得,那你可错了。Lambda函数结合了两者的优点,让你写出优雅简洁的代码。

1.3语法格式

Lambda 表达式就是一个可调用的代码单元,我们可以将其理解为一个未命名的内联函数。与任何函数类似,一个Lambda具有一个返回类型、一个参数列表和一个函数体。但与函数不同,Lambda可以定义在函数内部,其语法格式如下:

[capture list](parameter list) mutable(可选) 异常属性->return type{function body}

capture list(捕获列表)是一个Lambda所在函数中定义的局部变量的列表,通常为空,表示Lambda不使用它所在函数中的任何局部变量。parameter list(参数列表)、return type(返回类型)、function body(函数体)与任何普通函数基本一致,但是Lambda的参数列表不能有默认参数,且必须使用尾置返回类型。 mutable表示Lambda能够修改捕获的变量,省略了mutable,则不能修改。异常属性则指定Lambda可能会抛出的异常类型。

其中Lambda表达式必须的部分只有capture list和function body。在Lambda忽略参数列表时表示指定一个空参数列表,忽略返回类型时,Lambda可根据函数体中的代码推断出返回类型。例如:

auto f=[]{return 42;}

我们定义了一个可调用对象f,它不接受任何参数,返回42。auto关键字实际会将 Lambda 表达式转换成一种类似于std::function的内部类型(但并不是std::function类型,虽然与std::function“兼容”)。所以,我们也可以这么写:

std::function<int()> Lambda = [] () -> int { return val * 100;};

如果你对std::function<int()>这种写法感到很神奇,可以查看 C++ 11 的有关std::function的用法。简单来说,std::function<int()>是一个实例化后的模板类,代表一个可调用的对象,接受 0 个参数,返回值是int。所以,当我们需要一个接受一个double作为参数,返回int的对象时,就可以写作:std::function<int(double)>[3]^{[3]}[3]。

1.4调用方式

Lambda表达式的调用方式与普通函数的调用方式相同,上面Lambda表达式的调用方式如下:

cout<<f()<<endl;  //打印42

//或者直接调用
cout<<[]{return 42;}()<<endl;

我们还可以定义一个单参数的Lambda,实现上面字符串排序的shorter()比较函数的功能:

auto f=[](cosnt string& a,const string& b)
{
 return a.size()<b.size();
}

//将Lambda传入排序算法sort中
sort(words.begin(),word2.end(),[](cosnt string& a,const string& b){
 return a.size()<b.size();
});

//或者
sort(words.begin(),word2.end(),f);

2.Lambda的捕获列表

Lambda可以获取(捕获)它所在作用域中的变量值,由捕获列表(capture list)指定在Lambda 表达式的代码内可使用的外部变量。比如虽然一个Lambda可以出现在一个函数中,使用其局部变量,但它只能使用那些在捕获列表中明确指明的变量。Lambda在捕获所需的外部变量有两种方式:引用和值。我们可以在捕获列表中设置各变量的捕获方式。如果没有设置捕获列表,Lambda默认不能捕获任何的变量。捕获方式具体有如下几种:

  • [] 不截取任何变量
  • [&} 截取外部作用域中所有变量,并作为引用在函数体中使用
  • [=] 截取外部作用域中所有变量,并拷贝一份在函数体中使用
  • [=,&valist]   截取外部作用域中所有变量,并拷贝一份在函数体中使用,但是对以逗号分隔valist使用引用
  • [&,valist] 以引用的方式捕获外部作用域中所有变量,对以逗号分隔的变量列表valist使用值的方式捕获
  • [valist] 对以逗号分隔的变量列表valist使用值的方式捕获
  • [&valist] 对以逗号分隔的变量列表valist使用引用的方式捕获
  • [this] 截取当前类中的this指针。如果已经使用了&或者=就默认添加此选项。

在[]中设置捕获列表,就可以在Lambda中使用变量a了,这里使用按值(=, by value)捕获。

#include <iostream>

int main()
{
 int a = 123;
 auto lambda = [=]()->void
 {
 std::cout << "In Lambda: " << a << std::endl;
 };
 lambda();
 return 0;
}

编译运行结果如下:

In Lambda: 123

按值传递到Lambda中的变量,默认是不可变的(immutable),如果需要在Lambda中进行修改的话,需要在形参列表后添加mutable关键字(按值传递无法改变Lambda外变量的值)。

#include <iostream>
int main()
{
 int a = 123;
 std::cout << a << std::endl;
 auto lambda = [=]() mutable ->void{
 a = 234;
 std::cout << "In Lambda: " << a << std::endl;
 };
 lambda();
 std::cout << a << std::endl;
 return 0;
}

编译运行结果为:

123
In Lambda: 234  //可以修改
123             //注意这里的值,并没有改变

如果没有添加mutable,则编译出错:

$ g++ main.cpp -std=c++11
main.cpp:9:5: error: cannot assign to a variable captured by copy in a non-mutable Lambda
    a = 234;
                ~ ^
1 error generated.

看到这,不禁要问,这魔法般的变量捕获是怎么实现的呢?原来,Lambda是通过创建个类来实现的。这个类重载了操作符(),一个Lambda函数是该类的一个实例。当该类被构造时,周围的变量就传递给构造函数并以成员变量保存起来,看起来跟函数对象(仿函数)很相似,但是C++11标准建议使用Lambda表达式,而不是函数对象,Lambda表达式更加轻量高效,易于使用和理解[4]^{[4]}[4]。

3.Lambda的类型

lambda函数的类型看起来和函数指针很像,都是把函数赋值给了一个变量。实际上,lambda函数是用仿函数实现的,它看起来又像是一种自定义的类。而事实上,lambda类型并不是简单的函数指针类型或者自定义类型,lambda函数是一个闭包(closure)的类,C++11标准规定,closure类型是特有的、匿名且非联合体的class类型。每个lambda表达式都会产生一个闭包类型的临时对象(右值)。因此,严格来说,lambda函数并非函数指针,但是C++11允许lambda表达式向函数指针转换,前提是没有捕捉任何变量且函数指针所指向的函数必须跟lambda函数有相同的调用方式。

typedef int(*pfunc)(int x, int y);

int main()
{
 auto func = [](int x, int y)->int {
 return x + y;
 };
 pfunc p1 = nullptr;
 p1 = func;  //lambda表达式向函数指针转换

 std::cout << p1(1, 2) << std::endl;

 return 0;
}

4.lambda的常量性和mutable关键字

C++11中,默认情况下lambda函数是一个const函数,按照规则,一个const成员函数是不能在函数体内改变非静态成员变量的值。

int main()
{
 int val = 0;
 auto const_val_lambda = [=] { val = 3; }; // 编译失败,不能在const的lambda函数中修改按值捕获的变量val

 auto mutable_val_lambda = [=]() mutable { val = 3; };

 auto const_ref_lambda = [&] { val = 3; };

 auto const_param_lambda = [](int v) { v = 3; };
 const_param_lambda(val);

 return 0;
}

阅读代码,注意以下几点:
 (1)可以看到在const的lambda函数中无法修改按值捕捉到的变量。lambda函数是通过仿函数来实现的,捕捉到的变量相当于是仿函数类中的成员变量,而lambda函数相当于是成员函数,const成员函数自然不能修改普通成员变量;
 (2)使用引用的方式捕获的变量在常量成员函数中值被更改则不会导致错误,其原因简单地说,由于const_ref_lambda 不会改变引用本身,而只会改变引用的值,所以编译通过;
 (3)使用mutable修饰的mutable_val_lambda,去除了const属性,所以可以修改按值方式捕获到的变量;
 (4)按值传递参数的const_param_lambda修改的是传入lambda函数的实参,当然不会有问题。

5.Lambda的常见用法

(1)Lambda函数和STL
 Lambda函数的引入为STL的使用提供了极大的方便。比如下面这个例子,当你想遍历一个vector的时候,原来你得这么写:

vector<int> v={1,2,3,4,5,6,7,8,9};

//传统的for循环
for ( auto itr = v.begin(), end = v.end(); itr != end; itr++ )
{ 
 cout << *itr; 
}

//函数指针
void printFunc(int v)
{
 cout<<v;
}
for_each(v.begin(),v.end(),printFunc);

//仿函数
struct CPrintFunc
{
 void operator() (int val)const { cout << val; }
};
for_each(v.begin(),v.end(),CPrintFunc());

现在有了Lambda函数你就可以这么写:

for_each(v.begin(),v.end(),[](int val)
{ 
 cout << val;
});

很明显,相比于传统的for循环、函数指针和仿函数,使用lambda函数更加简洁。如果处理vector成员的业务代码更加复杂,那么更能凸显Lambda函数的便捷。而且这么写之后执行效率反而会提高,因为编译器有可能使用循环展开来加速执行过程。

以上就是一文读懂c++11 Lambda表达式的详细内容,更多关于c++11 Lambda表达式的资料请关注脚本之家其它相关文章!

更多精彩内容其他人还在看

用标准c++实现string与各种类型之间的转换

这个类在头文件中定义, < sstream>库定义了三种类:istringstream、ostringstream和stringstream,分别用来进行流的输入、输出和输入输出操作。另外,每个类都有一个对应的宽字符集版本
收藏 0 赞 0 分享

C++如何通过ostringstream实现任意类型转string

再使用整型转string的时候感觉有点棘手,因为itoa不是标准C里面的,而且即便是有itoa,其他类型转string不是很方便。后来去网上找了一下,发现有一个好方法
收藏 0 赞 0 分享

C/C++指针小结

要搞清一个指针需要搞清指针的四方面的内容:指针的类型,指针所指向的类型,指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区
收藏 0 赞 0 分享

C++ 类的静态成员深入解析

在C++中类的静态成员变量和静态成员函数是个容易出错的地方,本文先通过几个例子来总结静态成员变量和成员函数使用规则,再给出一个实例来加深印象
收藏 0 赞 0 分享

C++类的静态成员初始化详细讲解

通常静态数据成员在类声明中声明,在包含类方法的文件中初始化.初始化时使用作用域操作符来指出静态成员所属的类.但如果静态成员是整型或是枚举型const,则可以在类声明中初始化
收藏 0 赞 0 分享

C++类静态成员与类静态成员函数详解

静态成员不可在类体内进行赋值,因为它是被所有该类的对象所共享的。你在一个对象里给它赋值,其他对象里的该成员也会发生变化。为了避免混乱,所以不可在类体内进行赋值
收藏 0 赞 0 分享

C++中的friend友元函数详细解析

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类。友元函数的特点是能够访问类中的私有成员的非成员函数。友元函数从语法上看,它与普通函数一样,即在定义上和调用上与普通函数一样
收藏 0 赞 0 分享

static全局变量与普通的全局变量的区别详细解析

以下是对static全局变量与普通的全局变量的区别进行了详细的分析介绍,需要的朋友可以过来参考下,希望对大家有所帮助
收藏 0 赞 0 分享

C++ explicit关键字的应用方法详细讲解

C++ explicit关键字用来修饰类的构造函数,表明该构造函数是显式的,既然有"显式"那么必然就有"隐式",那么什么是显示而什么又是隐式的呢?下面就让我们一起来看看这方面的知识吧
收藏 0 赞 0 分享

教你5分钟轻松搞定内存字节对齐

随便google一下,人家就可以跟你解释的,一大堆的道理,我们没怎么多时间,讨论为何要对齐.直入主题,怎么判断内存对齐规则,sizeof的结果怎么来的,请牢记以下3条原则
收藏 0 赞 0 分享
查看更多