C++(一篇文章让你会用 lambda 表达式)
lambda表达式
lambda表达式概述
我们可以向一个算法传递任何类型的可调用对象。对于一个对象或一个表达式,如果可以对其使用调用调用运算符,则称它为可调用的。即,如果 e 是一个可调用的表达式,则我们可以编写代码 e(args) ,其中 args 是一个逗号分隔的一个或多个参数的列表。
通常存在四种可调用对象:
- 函数
- 函数指针
- 重载了函数调用符的类
- lambda表达式
一个 lambda 表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。与任何函数类似,一个lambda具有一个返回类型、一个参数列表和一个函数体。但是与函数不同,lambda 可能定义在函数内部。
lambda表达式定义
lambda表达式示例
lambda有很多叫法,有lambda表达式、lambda函数、匿名函数,本文中为了方便表述统一用lambda表达式进行叙述。 ISO C ++标准官网展示了一个简单的lambda 表示式实例:
#include <algorithm>
#include <cmath>
void abssort(float* x, unsigned n) {
std::sort(x, x + n,
// Lambda expression begins
[](float a, float b) {
return (std::abs(a) < std::abs(b));
} // end of lambda expression
);
}
在上面的实例中std::sort函数第三个参数应该是传递一个排序规则的函数,但是这个实例中直接将排序函数的实现写在应该传递函数的位置,省去了定义排序函数的过程,对于这种不需要复用,且短小的函数,直接传递函数体可以增加代码的可读性。
lambda表达式语法
lambda表达式定义了一个匿名函数,并且可以捕获一定范围内的变量。语法形式如下:
[caputer list] (parameters) mutable throw() -> return type{ statement }
参数介绍:
- caputer list是捕获列表。在C++规范中也称为lambda导入器,捕获列表总是出现在lambda函数的开始处。实际上,[]是Lambda引出符。编译器根据该引出符判断接下来的代码是否是lambda函数,捕获列表能够捕捉上下文中的变量以供lambda函数使用。
- parameters是参数列表。与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号“()”一起省略。
- mutable关键字。默认情况下Lambda函数总是一个const函数,mutable可以取消其常量性。加上mutable关键字后,可以修改传递进来的拷贝,在使用该修饰符时,参数列表不可省略(即使参数为空)。
- throw是异常说明。用于Lamdba表达式内部函数抛出异常。
- return type是返回值类型。 追踪返回类型形式声明函数的返回类型。我们可以在不需要返回值的时候也可以连同符号”->”一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导。
- statement是Lambda表达式的函数体。内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。
我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体:
auto f = []{return 42;};
在此例中,我们定义了一个可调用对象f,它不接受参数,返回42。
//lambda的调用方式与普通函数的调用方式相同,都是使用调用运算符:
cout << f() << endl;//打印42
在lambda中忽略括号和参数列表等价于指定一个空参数列表。在此例中,当调用 f 时,列表参数时空的。如果忽略返回类型,lambda 根据函数体中的代码推断出返回类型。如果函数体只有一个return语句,则返回类型从返回的表达式的类型推断出来。否则,返回类型为void。
如果lambda的函数体包含任何单一return语句之外的内容,并未指定返回类型,则返回void。
lambda表达式各个参数详解
caputer list捕获列表
首先需要区分以及了解三个概念:值捕获、引用捕获和隐式捕获
值捕获
类似于参数传递,变量的捕获方式也可以是值或引用。与传值参数类似,采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在 lambda 创建时拷贝,而不是调用时拷贝:
void func()
{
size_t v1 = 42;//局部变量
//将 v1 拷贝到名为 f 的可调用对象
auto f = [v1]{return v1;};
v1 = 0;
auto j = f();//j为42;f保存了我们创建它时 v1 的拷贝
}
由于被捕获变量的值是在创建时拷贝,因此随后对其求改不会影响到 lambda 内对应的值。
引用捕获
我们定义 lambda 时可以采用引用方式捕获变量。例如:
void func1()
{
size_t v1 = 42;//局部变量
//对象 f2 包含 v1 的引用
auto f2 = [&v1]{return v1;};
v1 = 0;
auto j = f2();//j为0;f2保存v1的引用,而非拷贝
}
v1之前的 & 指出 v1 应该以引用方式捕获。一个以引用方式捕获的变量与其他任何类型的引用行为类似。当我们在 lambda 函数体内使用此变量时,实际上使用的时引用所绑定的对象。在本例中,当 lambda 返回 v1 时,它返回的而是 v1 指向的对象的值。
引用捕获和返回引用有着相同的问题和局限。如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在 lambda 执行的时候是存在的。lambda 捕获的都是局部变量,这些变量在函数结束后就不复存在了。如果 lambda 可能在函数结束后执行,捕获的引用指向的局部变量已经消失。
引用捕获有时是必要的。例如,我们可能希望 biggies 函数接受一个 ostream 的引用,用来输出数据,并接受一个字符作为分隔符:
void biggies(vecotr<string> &words, vecotr<string>::size_type sz, ostream &os = cout, char = ' ')
{
// 打印cout的语句改为打印到os
for_earch(words.begin(), words.end(), [&os, c](const string &s)
{
os << s << c ;
});
}
我们不能拷贝 ostream 对象,因此捕获 os 唯一的方式就是捕获其引用(或指向 os 的指针)。
当我们像一个函数传递一个 lambda 时,就像本例中调用 for_each 那样,lambda 会立刻执行。在此情况下,以引用方式捕获 os 就没问题,因为当 for_each 执行时,biggies 中的变量是存在的。
我们也可以从一个函数返回 lambda。函数可以直接返回一个可调用对象,或者返回一个类对象,该类含有可调用对象的数据成员。如果函数返回一个 lambda,则与函数不能返回一个局部变量的引用类似,此 lambda 也不能包含引用捕获。
当以引用方式捕获一个变量时,必须保证在 lambda 执行时变量是存在的。
建议:尽量保持 lambda 的变量捕获简单化
一个 lambda 捕获从 lambda被创建(即,定义 lambda 的代码执行时)到 lambda 自身执行(可能有多次执行)这段时间内保存的相关信息。确保 lambda 每次执行的时候这些信息都有预期的意义,是程序员的责任。
捕获一个普通变量,如 int, string 或其他非指针类型,通常可以采用简单的值捕获方式。在此情况下,只需关注变量在捕获时是否有我们所需的值就可以。
如果我们捕获一个指针或迭代器,或采用引用捕获方式,就必须确保在 lambda 执行时,绑定到迭代器、指针或引用的对象仍然存在。而且,需要保证对象具有预期的值。在 lambda 从创建到他执行的这段时间内,可能有代码改变绑定的对象的值。也就是说,在指针(或引用)被捕获的时刻,绑定的对象的值是我们所期望的,但在 lambda 执行时,该对象的值可能已经完全不同了。
一般来说,我们应该尽量减少捕获的数据量,来避免潜在的捕获导致的问题。而且如果可能的话,应该避免捕获指针或引用。
隐式捕获
除了显式的列出我们希望使用的来自所在函数的变量之外,还可以让编译器根据 lambda 体中的代码来推断我们要使用哪些变量。为了指示编译器推断捕获列表,应在捕获列表中写一个 & 或者 = 。& 告诉编译器采用捕获引用方式,= 则表示采用值捕获方式。例如,我们可以重写传递给 find_if 的 lambda:
//sz为隐式捕获,值捕获方式
wc = find_if(words.begin(), words.end(),
[=](const string &s)
{
return s.size() >= sz;
});
如果我们希望对一部分变量采用值捕获,对其他变量采用引用捕获,可以混合使用隐式捕获和显式捕获:
void biggies(vecotr<string> &words, vecotr<string>::size_type sz, ostream &os = cout, char c =' '){
//其他处理与前例一样
//os隐式捕获,引用捕获方式;c显示捕获,值捕获方式
for_each(words.begin(), words.end(), [&,c](const string &s){
os << s << c;
});
//os显式捕获,引用捕获方式;c隐式捕获,值捕获方式
for_each(words.begin(), words.end(), [=,&os](const string &s){
os << s << c;
});
}
当我们混合使用了隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个 & 或 =。此符号指定了默认捕获方式为引用或值。
当混合使用隐式捕获和显示捕获时,显示捕获的变量必须使用与隐式捕获不同的方式。即,如果隐式捕获是引用方式(使用了&)则显式捕获命名变量必须采用值方式,因此不能在其名字前使用 & 。类似的,如果隐式捕获采用的是值方式(使用了=),则显式捕获命名变量必须采用引用方式,即,在名字前使用&。
好了,在了解了这些概念之后,让我们看看具体的实例吧
lambda表达式与普通函数最大的区别是,除了可以使用参数以外,lambda函数还可以通过捕获列表访问一些上下文中的数据。具体地,捕捉列表描述了上下文中哪些数据可以被Lambda使用,以及使用方式(以值传递的方式或引用传递的方式)。语法上,在"[]"包括起来的是捕获列表,捕获列表由多个捕获项组成,并以逗号分隔。捕获列表有以下几种形式:
- []表示空捕获列表。lambda不能使用所在函数中的变量。一个lambda只有捕获变量就才能使用它们
auto func = ([]{
cout << "Hello World!" << endl;
}
);
func();//输出Hello World
- [names]names是一个逗号分隔的名字列表,这些名字都是lambda所在函数的局部变量。默认情况下,捕获列表中的变量都被拷贝。
int num = 100;
auto func = ([num]{
cout << num << endl;
}
);
func();//输出100
- [=]隐式捕获列表,采用值捕获方式捕获所有父作用域的变量(包括this)
int index = 1;
int num = 100;
auto func = ([=]{
cout << "index: "<< index << ", "
<< "num: "<< num << endl;
}
);
func();//输出1,100
- [&val]表示引用传递捕获变量var
int num = 100;
auto func = ([&num]{
num = 1000;
scout << "num: " << num << endl;
}
);
func();//输出1000
- [&]隐式捕获列表,采用引用捕获方式捕捉所有父作用域的变量(包括this)
int index = 1;
int num = 100;
auto func = ([&]{
num = 1000;
index = 2;
cout << "index: "<< index << ", "
<< "num: "<< num << endl;
}
);
func();//输出2,1000
- [this]表示值传递方式捕捉当前的this指针
#include <iostream>
using namespace std;
class Lambda
{
public:
void sayHello() {
cout << "Hello" << endl;
};
void lambda() {
auto func = [this]{
this->sayHello();
};
func();
}
};
int main()
{
Lambda demo;
demo.lambda();
}//输出Hello
- [=,identifier_list]identifier_list是一个逗号分隔的列表,包含0个或多个来自所在函数的变量。identifier_list中的变量采用引用方式捕获,而任何隐式捕获的变量都采用值方式捕获。identifier_list中的名字不能包括 this,这些名字必须都有&。例如[=,&a,&b]表示以引用传递的方式捕捉变量a和b,以值传递方式捕捉其他变量
int index = 1;
int num = 100;
auto func = ([=, &index, &num]{
num = 1000;
index = 2;
cout << "index: "<< index << ", "
<< "num: "<< num << endl;
}
);
func();
- [&, identifier_list]identifier_list是一个逗号分隔的列表,包含0个或多个来自所在函数的变量。这些变量采用值捕获方式,而任何隐式捕获的变量都采用引用方式捕获。identifier_list列表中的名字前面不能使用&。例如:[&,a,this]表示以值传递的方式及捕捉变量a和this,引用传递方式捕捉其他所有变量
注意: 捕捉列表不允许变量重复传递。下面一些例子就是典型的重复,会导致编译时期的错误。例如: - [=,a]这里已经以值传递方式捕捉了所有变量,但是重复捕捉a了,会报错的;
- [&,&this]这里&已经以引用传递方式捕捉了所有变量,再捕捉this也是一种重复。
如果lambda主体total
通过引用访问外部变量,并factor
通过值访问外部变量,则以下捕获子句是等效的:
[&total, factor]
[factor, &total]
[&, factor]
[factor, &]
[=, &total]
[&total, =]
lambda参数列表
与一个普通函数调用类似,调用一个lambda时给定的实参被用来初始化lambda的形参,通常,实参和形参的类型必须匹配。但与普通函数不同,lambda不能有默认参数。因此,一个lambda调用的实参数目永远与形参数目相等。一旦形参初始化完毕,就可以执行函数体了。
作为一个带参数的 lambda 的例子:
[](const string &a, const string &b)
{
return a.size() < b.size();
}
空列表表明此 lambda 不适用它所在函数中的任何局部变量,参数是 const string 的引用类型。lambda 的函数体时比较两个参数的 size() ,并根据两者的相对大小返回一个 bool 值。
区别一下下面这种情况
void func(const string &a, const string &b)
{
auto function = []()
{
return a.size() < b.size();//error,捕获失败,没有捕获到外部变量a和b
};
}
两种情况有本质的区别,第一种是在lambda表达式的参数列表传入参数,类似于函数的参数列表,表达式会自动捕获,而第二种的x和y参数实际上是外部变量,不属于lambda表达式内。
可变mutable关键字的lambda
默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。如果我们希望能改变一个被捕获的变量的值,就必须在参数列表首加上关键字 mutable。因此,可变 lambda 能省略参数列表:
void fcn1()
{
size_t v1 = 42;//局部变量
//f可以改变它所捕获的变量的值
auto f = [v1]()mutable{return ++v1;};
v1 = 0;
auto j = f();//j为43
}
一个引用捕获的变量是否(如往常一样)可以修改依赖于此引用指向的是一个 const 类型还是一个非 const 类型:
void fcn2()
{
size_t v1 = 42;//局部变量
//v1是一个非const变量的引用
//可以通过f2中的引用来改变它
auto f = [v1]()mutable{return ++v1;};
v1 = 0;
auto j = f();//j为1
}
异常说明
明确指出异常规范是在 C++11 中弃用的 C++ 语言功能。因此这里不建议不建议大家使用,这里也不细讲。
返回类型
默认情况下,如果一个lambda体内包含 return 之外的任何语句,则编译器假定此 lambda 返回void。与其他返回 void 的函数类型,被推断返回 void 的 lambda 不能返回值。
lambda 表达式的返回类型会自动推导。除非你指定了返回类型,否则不必使用关键字。返回型类似于通常的方法或函数的返回型部分。但是,返回类型必须在参数列表之后,并且必须在返回类型->之前包含类型关键字。如果 lambda 主体仅包含一个 return 语句或该表达式未返回值,则可以省略 lambda 表达式的 return-type 部分。如果 lambda 主体包含一个 return 语句,则编译器将从 return 表达式的类型中推断出 return 类型。否则,编译器将返回类型推导为void。
`auto x1 = [](int i){ return i; };
当我们要为 lambda 定义返回类型时,必须使用位置返回类型:
[](int i) -> int{
if(i < 0)
{
return -i;
}
else{
return i;
}
}
lambda函数体
lambda表达式的lambda主体(标准语法中的_复合语句_)可以包含普通方法或函数的主体可以包含的任何内容。普通函数和lambda表达式的主体都可以访问以下类型的变量:
- 捕获变量
- 形参变量
- 局部声明变量
- 类数据成员,当在类内声明'this' 并被捕获时可以访问
- 具有静态存储持续时间的任何变量,例如全局变量
#define _CRT_SECURE_NO_WARNINGS
#include<iostream> //引入头文件
#include<cmath>
#include<cstring>//C++中的字符串
using namespace std; //标准命名空间
static int a = 10;//全局静态变量
class Lambda
{
public:
void lambda() {
//类数据成员,通过捕获this并访问
auto func = [this] {
cout << "类数据成员" << this->d << endl;;
};
func();
}
private:
int d = 50;
};
int main() {
static int b = 20;//局部静态变量
int m = 30;//捕获的变量
int n = 40;//捕获的变量
auto func = [m, n](int c) {
cout << "静态全局变量a:" << a << endl;
cout <<"静态局部变量b:" << b << endl;
cout << "形参变量c:" << c << endl;
cout << "捕获的变量m:" << m << endl;
cout << "捕获的变量n:" << n << endl;
};
//传入的形参变量
func(60);
Lambda bda;
bda.lambda();
system("pause");
return EXIT_SUCCESS;
}
lambda表达式的优缺点
lambda表达式的优点
-
可以直接在需要调用函数的位置定义短小精悍的函数,而不需要预先定义好函数
find_if(v.begin(), v.end(), [](int& item){return item > 2});
-
使用Lamdba表达式变得更加紧凑,结构层次更加明显、代码可读性更好
lambda表达式的缺点
- lamdba表达式语法比较灵活,增加了阅读代码的难度
- 对于函数复用无能为力
lambda表达式工作原理
当我们定义一个 lambda 的时候,编译器会生成一个与 lambda 对应的新的(未命名)类类型。编译器将该 lambda 表达式翻译成一个未命名类的未命名对象。在 lambda 表达式产生的类中含有一个重载的函数调用运算符,例如:
stable_sort(words.begin(), words.end(),
[](const string &a, const string &b)
{
return a.size() < b.size();
});
其行为类似于下面这个类的未命名对象
class ShorterString
{
public:
bool operator()(const string &s1, const string &s2) const
{
return s1.size() < s2.size();
}
};
产生的类只有一个函数调用运算符成员,它负责接受两个 string 并比较它们的长度,它们的形参列表和函数体与 lambda 表达式完全一样。默认情况下的 lambda 不能改变它捕获的变量。因此在默认情况下,由 lambda 产生的类当中的函数调用符是一个 const 成员函数。如果 lambda 被声明为可变(mutable)的,则调用运算符就不是 const 的了。
用这个类替代 lambda 表达式后,我们可以重写并调用 stable_sort:
stable_sort(words.begin(), words.end(), ShorterString());
第三个实参是新构建的 ShorterString 对象,当 stable_sort 内部的代码每次比较两个 string 时就会"调用"这一对象,此时该对象将调用运算符的函数体,判断第一个 string 的大小小于第二个时返回true。
如我们所知,当 lambda 表达式通过引用捕获变量时,将由程序负责确保 lambda 执行时引用所引的对象确实存在。因此,编译器可以直接使用该引用而无须在 lambda 产生的类中将其存储为数据成员。
相反,通过值捕获的变量被拷贝到 lambda 中。因此,这种 lambda 产生的类必须为每个值捕获的变量建立相应的数据成员,同时创建构造函数,令其使用捕获的变量的值来初始化数据成员。举个例子:
//获得第一个指向满足条件元素的迭代器,该元素满足 size() is >= sz
auto wc = find_if(words.begin(), words.end(), [sz](const string &a)
{
return a.size() >= sz;
});
该 lambda 表达式产生的类将形如:
class SizeComp
{
SizeComp(size_t n): sz(n){} //该形参对应捕获的变量
//该调用运算符的返回类型、形参和函数体都与 lambda 一致
bool operator()(const string &s) const
{
return s.size() >= sz;
}
private:
size_t sz; //该数据成员对应通过值捕获的变量
};
和我们的 ShorterString 类不同,上面这个类含有一个数据成员以及一个用于初始化该成员的构造函数。这个合成的类不含有默认构造函数,因此要想使用这个类必须提供一个实参:
//获得第一个指向满足条件的迭代器,该元素满足size() is >= sz
auto wc = find_if(words.begin(), words.end(), SizeComp(sz));
lambda 表达式产生的类不含有默认构造函数、赋值运算符以及默认析构函数;它是否含有默认的拷贝/移动构造函数则通常要视捕获的数据成员类型而定。
接下来我们来看一些实例并总结一下:编译器实现 lambda 表达式大致分为一下几个步骤
- 创建 lambda匿名类,实现构造函数,使用 lambda 表达式的函数体重载 operator()(所以lambda表达式也叫匿名函数对象)
- 创建lambda对象
- 通过对象调用operator()
举个例子:
void LambdaDemo()
{
int a = 1;
int b = 2;
auto lambda = [a, b](int x, int y)mutable throw() -> bool
{
return a + b > x + y;
};
bool ret = lambda(3, 4);
}
编译器会把上面这一句lambda表达式翻译为下面的代码:
class lambda_xxxx
{
private:
int a;
int b;
public:
lambda_xxxx(int _a, int _b) :a(_a), b(_b)
{
}
bool operator()(int x, int y) throw()
{
return a + b > x + y;
}
};
void LambdaDemo()
{
int a = 1;
int b = 2;
lambda_xxxx lambda = lambda_xxxx(a, b);
bool ret = lambda.operator()(3, 4);
}
其中,类名lambda_xxxx的xxxx是为了防止命名冲突加上的
- lambda表达式中的捕获列表,对应lambda_xxxx类的private 成员
- lambda表达式中的形参列表,对应lambda_xxxx类成员函数operator()的形参列表
- lambda表达式中的mutable,表明lambda_xxxx 类成员函数operator()的是否具有常属性 const,即是否是常成员函数
- lambda表达式中的返回类型,对应lambda_xxxx类成员函数operator()的返回类型
- lambda表达式中的函数体,对应lambda_xxxx类成员函数operator()的函数体
另外,lambda表达式捕获列表的捕获方式,也影响对应lambda_xxxx类的private成员的类型
- 值捕获:private成员的类型与捕获变量的类型一致
- 引用捕获:private成员的类型是捕获变量的引型
看到上面这种转化,是不是有一种似曾相识的感觉,仿函数也是这样的,仿函数(functor)又称为函数对象(function object)是一个能行使函数功能的类。仿函数的语法几乎和我们普通的函数调用一样,不过作为仿函数的类,都必须重载operator()运算符,仿函数与Lamdba表达式的作用是一致的。举个例子:
#include <iostream>
#include <string>
using namespace std;
class Functor
{
public:
void operator() (const string& str) const
{
cout << str << endl;
}
};
int main()
{
Functor myFunctor;
myFunctor("Hello world!");
return 0;
}
所以lambda表达式我们可以认为它是一个带有operator()的类,也就是仿函数
但有一种特殊情况需要说明:
如果lambda表达式不捕获任何外部变量,且有lambda_xxxx类到函数指针的类型转换,会有额外的代码生成,例如:
typedef int(_stdcall *Func)(int);
int Test(Func func)
{
return func(1);
}
void LambdaDemo()
{
Test([](int i) {
return i;
});
}
Test函数接受一个函数指针作为参数,并调用这个函数指针。实际调用Test时,传入的参数却是一个 lambda表达式,所以这里有一个类型的隐式转换:lambda_xxxx => 函数指针。
上面已经提到,Lambda表达式就是一个 lambda_xxxx 类的匿名对象,与函数指针之间按理说不应该存在转换,但是上述代码却没有问题。
其问题关键在于,上述代码中,lambda 表达式没有捕获任何外部变量,即lambda_xxxx 类没有任何成员变量,在operator()中也就不会用到任何成员变量,也就是说,operator()虽然是个成员函数,它却不依赖this就可以调用。
因为不依赖this,所以一个lambda_xxxx类的匿名对象与函数指针之间就存在转换的可能。
大致过程如下:
- 在lambda_xxxx类中生成一个静态函数,静态函数的函数签名与operator()一致,在这个静态函数中,通过一个空指针去调用该类的operator()
- 在lambda_xxxx重载与函数指针的类型转换操作符,在这个函数中,返回静态函数的地址。
typedef int(_stdcall *Func)(int);
class lambda_xxxx
{
private:
//没有捕获任何外部变量,所有没有成员
public:
/*...省略其他代码...*/
int operator()(int i)
{
return i;
}
static int _stdcall lambda_invoker_stdcall(int i)
{
return ((lambda_xxxx *)nullptr)->operator()(i);
}
operator Func() const
{
return &lambda_invoker_stdcall;
}
};
int Test(Func func)
{
return func(1);
}
void LambdaDemo()
{
auto lambda = lambda_xxxx ();
Func func = lambda.operator Func();
Test(func);
}