C++11 | Lambda表达式
什么是Lambda?
先来直观的看下Lambda表达式:
vector<int> v = {50, -10, 20, -30};
std::sort(v.begin(), v.end()); // the default sort
// now v should be { -30, -10, 20, 50 }
// sort by absolute value:
std::sort(v.begin(), v.end(), [](int a, int b) { return abs(a) < abs(b); });
// now v should be { -10, 20, -30, 50 }
在上述代码段中, sort的第三个参数 [](int a, int b) { return abs(a) < abs(b); } 就被称作Lambda表达式.
它传入两个参数a, b并返回它们绝对值比较的结果.
为什么要学习Lambda, 它和普通的内联函数有什么区别?
(1)Lambda定义和使用可以在同一个地方进行的, 便于查阅、调试代码;
(2)Lambda可访问作用域内的任何变量, 也可以指定某些函数中的局部变量(不可全局)交由Lambda捕获;
(3)C++11引入lambda的主要目的是,能够将类似于函数的表达式用作接受函数指针或函数符的函数的参数;
(4)典型的Lambda表达式是测试表达式或比较表达式,可编写一条返回语句;
(5)Lambda表达式不能有默认实参; C++14起, Lambda能拥有自身的默认实参
(6)如果Lambda中忽略返回类型, Lambda也可以自动推断返回类型.
Lambda表达式的一般形式
[capture-list] (parameter-list) mutable(optional) constexpr(optional)(c++17)
exception attribute -> return type { function body }
// C++20中形式如下:
[ captures ] <tparams>(optional)(c++20) ( params ) specifiers
exception attr -> ret requires(optional)(c++20) { function body }
其中:
capture-list:捕获列表,不能省略. 捕获列表总是出现于Lambda表达式的开始处, 是Lambda的引出符. 编译器可依据[]来推断该函数是否为Lambda函数. 同时"捕获列表"能够捕捉使用该表达式的函数中的局部变量, 将变量在Lambda函数体中使用.
Lambda函数能够捕获lambda函数外的具有自动存储时期的变量。函数体与这些变量的集合起来叫闭包。
捕获的方式可以是引用, 也可以是复制, 像下面这样:
[]:默认不捕获任何变量;
[=]:捕获外部作用域中所有变量,并拷贝一份在函数体中使用(值捕获);
[&]:捕获外部作用域中所有变量,并作为引用在函数体中使用(引用捕获);
[x]:仅以值捕获x,其它变量不捕获;
[&x]:仅以引用捕获x,其它变量不捕获;
[=, &x]:捕获外部作用域中所有变量,默认是值捕获,但是x是例外,通过引用捕获;
[&, x]:默认以引用捕获所有变量,但是x是例外,通过值捕获;
[this]:通过引用捕获当前对象(其实是复制指针);
[*this]:通过传值方式捕获当前对象.
我们可以忽略参数列表和返回类型, 但必须包含捕获列表和函数体, 所以一个简单的Lambda表达式像这样:
auto f = [] { return 66; };
parameter-list: 参数列表, 和普通函数传参一样, 可以省略. 这部分对于Lambda表达式是可选的, 故()也可以省略.
mutable:可选关键字,将Lambda表达式标记为mutable后,函数体就可以修改传值方式捕获的变量. 需要注意的是, 当我们在参数列表后面注明了“mutable”关键字之后,则可以取消其类似于class中const成员函数性质. 若在lambda中使用了mutable修饰符,则“参数列表”是不可省略掉的(即使是参数为空).
例如:
这里输出的k仍然是5, 但是去掉mutable之后:
在其中就无法修改k值.
但如果通过传引用的方式捕获变量, 就不受mutable限制, 且能够修改局部变量值:
像这样, 输出为7.
constexpr:可选,C++17,可以指定lambda表达式是一个常量函数;
exception:可选,指定lambda表达式可以抛出的异常;
attribute:可选,指定lambda表达式的特性;
return type:可选,返回值类型;和C/C++中的普通函数返回值类型的性质一样。主要目的是用来追踪lambda函数(有返回值情况下)的返回类型。可省略掉这一部分用于Lambda自动推导.
function body:函数体。在该函数体中,除了可以使用参数列表中的变量外,还可以使用所有捕获到的变量(即[capture-list] 中的变量).
对于以上参数, 总结如下:
Lambda表达式前面的[]赋予了Lambda表达式很强大的功能, 就是闭包.
Lambda表达式的大致原理:每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符), 我们称为闭包类型(closure type).
class ClosureType {
public:
// ...
ReturnType operator(params) const { body };
}
如果带上mutable关键字, 就像这样:
class ClosureType {
public:
// ...
ReturnType operator(params) { body };
}
在运行时,这个lambda表达式就会返回一个匿名的闭包实例,其实一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式以及变量,我们又将其称为Lambda捕捉块.
需要注意的是, Lambda表达式无法进行赋值. 但能够生成副本.
auto a = [] { cout << "A" << endl; };
auto b = [] { cout << "B" << endl; };
a = b; // 非法,lambda无法赋值
auto c = a; // 合法,生成一个副本
// 原因是Lambda中禁用了赋值运算符, 在class中类似这样:
ClosureType& operator=(const ClosureType&) = delete;
最后, 一个综合实例如下:
在STL中, 我们也可以将Lambda表达式传入容器作为比较依据.
//
// Created by hellcat on 2020.05.27.
//
#include <iostream>
#include <set>
using namespace std;
class Person {
public:
Person(const string& first, const string& last) :
m_firstName(first), m_lastName(last) {}
string getFirstName() const { return m_firstName; }
string getLastName() const { return m_lastName; }
private:
string m_firstName;
string m_lastName;
};
int main() {
auto cmp = [](const Person& p1, const Person& p2) {
return p1.getLastName() < p2.getLastName() ||
(p1.getLastName() == p2.getLastName() &&
p1.getFirstName() < p2.getFirstName());
};
set<Person, decltype(cmp)> coll(cmp);
// 调用的构造函数为
// explicit
// set(const _Compare& __comp,
// const allocator_type& __a = allocator_type())
// : _M_t(__comp, _Key_alloc_type(__a)) { }
// 如果调用set默认构造函数, 则会唤醒cmp构造函数, 而Lambda表达式不存在构造函数
coll.insert(Person("Lin", "YANG"));
coll.insert(Person("Li", "YANG"));
coll.insert(Person("hellcat", "shelby"));
cout<<coll.begin()->getFirstName()<<endl;
}
// 输出为Li.
在这里如果使用set默认构造函数, 则会有:
参考: https://zh.wikipedia.org/wiki/闭包_(计算机科学)
https://en.cppreference.com/w/cpp/language/lambda