随着时代的发展,许多为了便利开发者的特性被加入到开发语言中,这是一种趋势,意味着编程语言不再向机器而是向开发人员倾斜,但减轻开发人员的负担意味着额外的支出,效率、编译器的复杂性、潜在的问题等等都会越来越多,本质上,开发是一种工程,在整体不变的情况下,这是一个零和的博弈。
例如,在32位的时代,程序员们几乎可以将系统的资源使用能力发挥到极致,地址空间安排的可以非常紧凑,并有异常惊人的技巧性和灵活性,将一些不可思议的实现付诸现实,这背后的代价是需要非常优秀的程序员,有着非必常人的创造力,对编程语言非常精通,对各种技术信手拈来,这样的程序员并不多见;然而在64位的时候,由于几乎无限的地址空间和运算资源,导致上一个时代的非常多的技巧都无效了,大部分程序可以平A过去。
以前的程序员可能需要考虑很多的问题: 程序分配了多少内存?开辟多少线程?线程间的同步技巧有哪些?如何高效的使用内存?如何设计并实现高并发和低延迟的系统?如何将系统的资源使用率卡在90%又不至于引起用户的不适?这些技巧以前是入门级,现在则是精通级别了,这也许是一件好事,但学习和了解新技术,要关注新技术背后的实现。
但很多程序并不需要非常强的健壮性和安全性,在某种情况下,一个工具适应一个场景,也不是那么不可接受,这取决于具体的项目和实践活动,例如Lambda就是是一个新加入的特性。
Lambda 表达式(lambda expression)是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包,注意和数学传统意义上的不同。
Lambda 表达式的各个部分
下面是一个简单的 Lambda,它作为第三个参数传递给 std::sort() 函数:
#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
);
}
此图显示了 Lambda 语法的组成部分:
- capture 子句,在 C++ 规范中也称为 Lambda 引导;
- 参数列表(可选)。 也称为 Lambda 声明符;
- mutable 规范(可选);
- exception-specification(可选);
- trailing-return-type(可选);
- Lambda 体;
capture 子句
Lambda 可在其主体中引入新的变量(用 C++14),它还可以访问(或“捕获”)周边范围内的变量。 Lambda 以 capture 子句开头。 它指定捕获哪些变量,以及捕获是通过值还是通过引用进行的。 有与号 (&) 前缀的变量通过引用进行访问,没有该前缀的变量通过值进行访问。
空 capture 子句 [ ] 指示 lambda 表达式的主体不访问封闭范围中的变量。
可以使用默认捕获模式来指示如何捕获 Lambda 体中引用的任何外部变量:[&] 表示通过引用捕获引用的所有变量,而 [=] 表示通过值捕获它们。 可以使用默认捕获模式,然后为特定变量显式指定相反的模式。 例如,如果 lambda 体通过引用访问外部变量 total 并通过值访问外部变量 factor,则以下 capture 子句等效:
[&total, factor]
[factor, &total]
[&, factor]
[=, &total]
使用默认捕获时,只有 Lambda 体中提及的变量才会被捕获。
如果 capture 子句包含默认捕获 &,则该 capture 子句的捕获中没有任何标识符可采用 &identifier 形式。 同样,如果 capture 子句包含默认捕获 =,则该 capture 子句没有任何捕获可采用 =identifier 形式。 标识符或 this 在 capture 子句中出现的次数不能超过一次。 以下代码片段给出了一些示例:
struct S { void f(int i); };
void S::f(int i) {
[&, i]{}; // OK
[&, &i]{}; // ERROR: i preceded by & when & is the default
[=, this]{}; // ERROR: this when = is the default
[=, *this]{ }; // OK: captures this by value. See below.
[i, i]{}; // ERROR: i repeated
}
捕获后跟省略号是一个包扩展,如以下可变参数模板示例中所示:
template<class... Args>
void f(Args... args) {
auto x = [args...] { return g(args...); };
x();
}
要在类成员函数体中使用 Lambda 表达式,请将 this 指针传递给 capture 子句,以提供对封闭类的成员函数和数据成员的访问权限。
Visual Studio 2017 版本 15.3 及更高版本(在 /std:c++17 模式及更高版本中可用):可以通过在 capture 子句中指定 *this 通过值捕获 this 指针。 通过值捕获会将整个闭包复制到调用 Lambda 的每个调用站点。 闭包是封装 Lambda 表达式的匿名函数对象。当 Lambda 在并行或异步操作中执行时,通过值捕获非常有用。 它在某些硬件体系结构(如 NUMA)上特别有用。
在使用 capture 子句时,建议你记住以下几点,尤其是使用采取多线程的 Lambda 时:
引用捕获可用于修改外部变量,而值捕获却不能实现此操作。 mutable 允许修改副本,而不能修改原始项。
引用捕获会反映外部变量的更新,而值捕获不会。
引用捕获引入生存期依赖项,而值捕获却没有生存期依赖项。 当 Lambda 以异步方式运行时,这一点尤其重要。 如果在异步 Lambda 中通过引用捕获局部变量,该局部变量将很容易在 Lambda 运行时消失。 代码可能会导致在运行时发生访问冲突。
通用捕获 (C++14)
在 C++14 中,可在 Capture 子句中引入并初始化新的变量,而无需使这些变量存在于 Lambda 函数的封闭范围内。 初始化可以任何任意表达式表示;且将从该表达式生成的类型推导新变量的类型。 借助此功能,你可以从周边范围捕获只移动的变量(例如 std::unique_ptr)并在 Lambda 中使用它们。
pNums = make_unique<vector<int>>(nums);
//...
auto a = [ptr = move(pNums)]()
{
// use ptr
};
参数列表
Lambda 既可以捕获变量,也可以接受输入参数。 参数列表(在标准语法中称为 Lambda 声明符)是可选的,它在大多数方面类似于函数的参数列表。
auto y = [] (int first, int second)
{
return first + second;
};
在 C++14 中,如果参数类型是泛型,则可以使用 auto 关键字作为类型说明符。 此关键字将告知编译器将函数调用运算符创建为模板。 参数列表中的每个 auto 实例等效于一个不同的类型参数。
auto y = [] (auto first, auto second)
{
return first + second;
};
Lambda 表达式可以将另一个 Lambda 表达式作为其自变量。
由于参数列表是可选的,因此在不将自变量传递到 Lambda 表达式,并且其 Lambda 声明符不包含 exception-specification、trailing-return-type 或 mutable 的情况下,可以省略空括号。