lambda表达式
Lambda表达式概述
- Lambda表达式是现代C++在C ++ 11和更高版本中的一个新的语法糖 ,在C++11、C++14、C++17和C++20中Lambda表达的内容还在不断更新。 lambda表达式(也称为lambda函数)是在调用或作为函数参数传递的位置处定义匿名函数对象的便捷方法。通常,lambda用于封装传递给算法或异步方法的几行代码 。
- Lambda有很多叫法,有Lambda表达式、Lambda函数、匿名函数。
Lambda表达式示例
商品类Goods的定义如下:
struct Goods
{
string _name; //名字
double _price; //价格
int _num; //数量
};
现在要对若干商品分别按照价格和数量进行升序、降序排序。
- 要对一个数据集合中的元素进行排序,可以使用sort函数,但由于这里待排序的元素为自定义类型,因此需要用户自行定义排序时的比较规则。
- 要控制sort函数的比较方式常见的有两种方法,一种是对商品类的的()运算符进行重载,另一种是通过仿函数来指定比较的方式。
- 显然通过重载商品类的()运算符是不可行的,因为这里要求分别按照价格和数量进行升序、降序排序,每次排序就去修改一下比较方式是很笨的做法。
所以这里选择传入仿函数来指定排序时的比较方式。比如:
struct ComparePriceLess
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._price < g2._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._price > g2._price;
}
};
struct CompareNumLess
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._num < g2._num;
}
};
struct CompareNumGreater
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._num > g2._num;
}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 300 }, { "香蕉", 3.3, 100 }, { "橙子", 2.2, 1000 }, { "菠萝", 1.5, 1 } };
sort(v.begin(), v.end(), ComparePriceLess()); //按价格升序排序
sort(v.begin(), v.end(), ComparePriceGreater()); //按价格降序排序
sort(v.begin(), v.end(), CompareNumLess()); //按数量升序排序
sort(v.begin(), v.end(), CompareNumGreater()); //按数量降序排序
return 0;
}
仿函数确实能够解决这里的问题,但可能仿函数的定义位置可能和使用仿函数的地方隔得比较远,这就要求仿函数的命名必须要通俗易懂,否则会降低代码的可读性。
对于这种场景就比较适合使用lambda表达式。比如:
int main()
{
vector<Goods> v = { { "苹果", 2.1, 300 }, { "香蕉", 3.3, 100 }, { "橙子", 2.2, 1000 }, { "菠萝", 1.5, 1 } };
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)
{
return g1._price < g2._price;
}); //按价格升序排序
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)
{
return g1._price > g2._price;
}); //按价格降序排序
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)
{
return g1._num < g2._num;
}); //按数量升序排序
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)
{
return g1._num > g2._num;
}); //按数量降序排序
return 0;
}
实例中直接将排序函数的实现写在应该传递函数的位置,省去了定义排序函数的过程,对于这种不需要复用,且短小的函数,直接传递函数体可以增加代码的可读性。
lambda表达式语法
lambda表达式书写格式:
[capture-list](parameters)mutable throw->return-type{statement}
- 捕获列表。在C++规范中也称为Lambda导入器, 捕获列表总是出现在Lambda函数的开始处。实际上,
[]
是Lambda引出符。编译器根据该引出符判断接下来的代码是否是Lambda函数,捕获列表能够捕捉上下文中的变量以供Lambda函数使用。 - 参数列表。与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号
“()”
一起省略。 - 可变规格。
mutable
修饰符, 默认情况下Lambda函数总是一个const函数,mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空)。 - 异常说明。用于Lamdba表达式内部函数抛出异常。
- 返回类型。 追踪返回类型形式声明函数的返回类型。我们可以在不需要返回值的时候也可以连同符号
”->”
一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导。 - lambda函数体。内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。
lambda函数的参数列表和返回值类型都是可选部分,但捕捉列表和函数体是不可省略的,因此最简单的lambda函数如下:
int main()
{
[]{}; //最简单的lambda表达式
return 0;
}
Lambda表达式参数详解
Lambda捕获列表
Lambda表达式与普通函数最大的区别是,除了可以使用参数以外,Lambda函数还可以通过捕获列表访问一些上下文中的数据。具体地,捕捉列表描述了上下文中哪些数据可以被Lambda使用,以及使用方式(以值传递的方式或引用传递的方式)。语法上,在“[]”
包括起来的是捕获列表,捕获列表由多个捕获项组成,并以逗号分隔。捕获列表有以下几种形式:
[]
表示不捕获任何变量
auto function = ([]{
std::cout << "Hello World!" << std::endl;
}
);
function();
[var]
表示值传递方式捕获变量var
int num = 100;
auto function = ([num]{
std::cout << num << std::endl;
}
);
function();
[&var]
表示引用传递捕捉变量var
int num = 100;
auto function = ([&num]{
num = 1000;
std::cout << "num: " << num << std::endl;
}
);
function();
[=]
表示值传递方式捕获所有父作用域的变量(包括this)
父作用域指的是包含lambda函数的语句块。
int index = 1;
int num = 100;
auto function = ([=]{
std::cout << "index: "<< index << ", "
<< "num: "<< num << std::endl;
}
);
function();
[&]
表示引用传递方式捕捉所有父作用域的变量(包括this)
int index = 1;
int num = 100;
auto function = ([&]{
num = 1000;
index = 2;
std::cout << "index: "<< index << ", "
<< "num: "<< num << std::endl;
}
);
function();
[this]
表示值传递方式捕捉当前的this指针
#include <iostream>
using namespace std;
class Lambda
{
public:
void sayHello() {
std::cout << "Hello" << std::endl;
};
void lambda() {
auto function = [this]{
this->sayHello();
};
function();
}
};
int main()
{
Lambda demo;
demo.lambda();
}
[=, &]
拷贝与引用混合[=, &a, &b]
表示以引用传递的方式捕捉变量a和b,以值传递方式捕捉其它所有变量。[&, a, this]
表示以值传递的方式捕捉变量a和this,引用传递方式捕捉其它所有变量。
int index = 1;
int num = 100;
auto function = ([=, &index, &num]{
num = 1000;
index = 2;
std::cout << "index: "<< index << ", "
<< "num: "<< num << std::endl;
}
);
function();
不过值得注意的是,捕捉列表不允许变量重复传递。下面一些例子就是典型的重复,会导致编译时期的错误。例如:
[=,a]
这里已经以值传递方式捕捉了所有变量,但是重复捕捉a了,会报错的;[&,&this]
这里&已经以引用传递方式捕捉了所有变量,再捕捉this也是一种重复。
如果Lambda主体total
通过引用访问外部变量,并factor
通过值访问外部变量,则以下捕获子句是等效的:
[&total, factor]
[factor, &total]
[&, factor]
[factor, &]
[=, &total]
[&total, =]
- 在块作用域以外的lambda函数捕捉列表必须为空,即全局lambda函数的捕捉列表必须为空。
- 在块作用域中的lambda函数仅能捕捉父作用域中的局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
任何一对花括号中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域。main函数就算一个块作用域。
Lambda参数列表
除了捕获列表之外,Lambda还可以接受输入参数。参数列表是可选的,并且在大多数方面类似于函数的参数列表。
auto function = [] (int first, int second){
return first + second;
};
function(100, 200);
可变规格mutable
我们看下面的例子:
int main()
{
int a = 10, b = 20;
auto Swap = [a, b]()
{
int tmp = a;
a = b;
b = tmp;
};
Swap(); //交换a和b?
return 0;
}
我们以传值方式进行捕捉a和b,然后再函数体中修改了a和b,其实代码编译不会通过,因为传值捕获到的变量默认是不可修改的。
如果要取消其常量性,就需要在lambda表达式中加上mutable,并且此时参数列表不可省略。比如:
int main()
{
int a = 10, b = 20;
auto Swap = [a, b]()mutable
{
int tmp = a;
a = b;
b = tmp;
};
Swap(); //交换a和b?
return 0;
}
上面的代码在语法上这回就没有问题了,但是逻辑上有问题,由于这里是传值捕捉,lambda函数中对a和b的修改不会影响外面的a、b变量,与函数的传值传参是一个道理,因此这种方法无法完成两个数的交换。
异常说明
你可以使用 throw()
异常规范来指示 Lambda 表达式不会引发任何异常。与普通函数一样,如果 Lambda 表达式声明 C4297 异常规范且 Lambda 体引发异常,Visual C++ 编译器将生成警告 throw() 。
int main() // C4297 expected
{
[]() throw() { throw 5; }();
}
在MSDN的异常规范中,明确指出异常规范是在 C++11 中弃用的 C++ 语言功能。因此这里不建议大家使用。
返回类型
- Lambda表达式的返回类型会自动推导。除非你指定了返回类型,否则不必使用关键字。
- 返回类型必须在参数列表之后,并且必须在返回类型之前包含类型关键字
->
。 - 如果Lambda主体仅包含一个return语句或该表达式未返回值,则可以省略Lambda表达式的return-type部分。如果Lambda主体包含一个return语句,则编译器将从return表达式的类型中推断出return类型。否则,编译器将返回类型推导为void。
auto x1 = [](int i){ return i; };
Lambda函数体
Lambda表达式的Lambda主体(标准语法中的复合语句)可以包含普通方法或函数的主体可以包含的任何内容。普通函数和Lambda表达式的主体都可以访问以下类型的变量:
- 捕获变量
- 形参变量
- 局部声明的变量
- 类数据成员,当在类内声明this并被捕获时
- 具有静态存储持续时间的任何变量,例如全局变量
#include <iostream>
using namespace std;
int main()
{
int m = 0;
int n = 0;
auto res=[&, n] (int a) mutable { m = ++n + a; };
res(4);
//上面两行也可以写成 [&, n] (int a) mutable { m = ++n + a; }(4);
cout << m << endl << n << endl;
}
lambda表达式底层原理
C++仿函数
仿函数(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表达式的处理方式,完全就是按照函数对象(仿函数)的方式处理的。就是在类中对()运算符进行了重载的类对象。
下面编写了一个Add类,该类对()运算符进行了重载,因此Add类实例化出的add1对象就叫做函数对象,add1可以像函数一样使用。然后我们编写了一个lambda表达式,并借助auto将其赋值给add2对象,这时add1和add2都可以像普通函数一样使用。比如:
class Add
{
public:
Add(int base)
:_base(base)
{}
int operator()(int num)
{
return _base + num;
}
private:
int _base;
};
int main()
{
int base = 1;
//函数对象
Add add1(base);
add1(1000);
//lambda表达式
auto add2 = [base](int num)->int
{
return base + num;
};
add2(1000);
return 0;
}
调试代码并转到反汇编,可以看到:
- 在创建函数对象add1时,会调用Add类的构造函数。
- 在使用函数对象add1时,会调用Add类的()运算符重载函数。
观察lambda表达式时,也能看到类似的代码:
- 借助auto将lambda表达式赋值给add2对象时,会调用
<lambda_uuid>
类的构造函数。 - 在使用add2对象时,会调用
<lambda_uuid>
类的()运算符重载函数。
本质就是因为lambda表达式在底层被转换成了仿函数。
当我们定义一个lambda表达式后,编译器会自动生成一个类,在该类中对()
运算符进行重载,实际lambda函数体的实现就是这个仿函数的operator()
的实现。
在调用lambda表达式时,参数列表和捕获列表的参数,最终都传递给了仿函数的operator()。
lambda表达式之间不能相互赋值
lambda表达式之间不能相互赋值,就算是两个一模一样的lambda表达式。
因为lambda表达式底层的处理方式和仿函数是一样的,在VS下,lambda表达式在底层会被处理为函数对象,该函数对象对应的类名叫做<lambda_uuid>
。
类名中的uuid
叫做通用唯一识别码(Universally Unique Identifier),简单来说,uuid
就是通过算法生成一串字符串,保证在当前程序当中每次生成的uuid都不会重复。
lambda表达式底层的类名包含uuid,这样就能保证每个lambda表达式底层类名都是唯一的。因此每个lambda表达式的类型都是不同的,这也就是lambda表达式之间不能相互赋值的原因,我们可以通过typeid(变量名).name()
的方式来获取lambda表达式的类型。比如:
int main()
{
int a = 10, b = 20;
auto Swap1 = [](int& x, int& y)->void
{
int tmp = x;
x = y;
y = tmp;
};
auto Swap2 = [](int& x, int& y)->void
{
int tmp = x;
x = y;
y = tmp;
};
cout << typeid(Swap1).name() << endl;
cout << typeid(Swap2).name() << endl;
return 0;
}
可以看到,就算是两个一模一样的lambda表达式,它们的类型都是不同的。
说明一下:
- 编译器只需要保证每个lambda表达式底层对应类的类名不同即可,并不是每个编译器都会将lambda表达式底层对应类的类名处理成<lambda_uuid>,这里只是以VS为例。
the end!