目录
背景介绍:
Lambda 表达式是从 C++ 11 开始引入并被广泛使用,在其版本和更高版本中是一种在被调用的位置或作为参数传递给函数接口的位置定义匿名函数对象即闭包的方法。
Lambda 通常用于封装传递给异步函数或某些算法的代码,其一般是由少量代码行所编写。该文章只是简单介绍 Lambda 的一些基础知识,高阶的知识分享会在之后的文章中出现。同时,该文章我参考了一些网上的介绍加上亲身的测试和理解。如有不正确的地方,请在评论区说明。
下面是一段伪代码,里面只是简单写了利用 lambda 作为第三个参数传递给函数和利用另外新增的函数进行调用的方法上的对比。
- #include
- #include
-
- using namespace std;
-
- // Method 1, not use lambda
- bool compareNumbers(uint8_t num1, uint_8 num2) {
- return num1 > num2;
- }
-
- // Method 2, use lambda
- // Lambda expression begins
- [](uint8_t num1, uint8_t num2) {
- return num1 > num2;
- }
- // End of lambda expression
-
- int main() {
- int initA = 1;
- uint_8 a = 10, b = 9;
- // Method 1
- sort(initA, initA + 10, compareNumbers(a, b));
- // Method 2
- sort(initA, initA + 10,
- [](uint8_t num1, uint8_t num2) {
- return num1 > num2;
- }
- );
- }
Lambda 的组成:
- [=] () mutable throw() -> bool {
- int c = a + b;
- a = b;
- return c > (a + b);
- }
-
- 1. [=] : This is capture words.
- 2. () : This is parameters list (Optional)
- 3. mutable : This is mutable specification (Optional)
- 4. throw() : This is exception-specification (Optional)
- 5. -> bool : This is trailing-return-type (Optional)
- 6. {...} : This is a body of lambda
C++ 14 中的 lambda 表达式可在其主体中引入新的变量,并可以捕获周边范围内的变量。 Lambda 以 capture 子句作为开头:
a. 指定捕获哪些变量
b.捕获是通过值还是通过引用进行的:
b1. 有与号 (&
) 前缀的变量将通过引用进行访问
b2. 没有该前缀的变量将通过值进行访问。
c. 空 capture 子句 [ ]
表示 lambda 的主体不访问封闭范围中的变量。
可以使用默认捕获模式来指示如何捕获 Lambda 体中引用的任何外部变量,使用默认捕获时,只有 lambda 体中所提及的变量才会被捕获:
a. [&]
表示通过引用捕获引用的所有变量,而 [=]
表示通过值捕获它们。
可以使用默认捕获模式,然后为特定变量显式指定相反的模式。 例如,如果 lambda 体通过引用访问外部变量 tot 并通过值访问外部变量 foo1
,则以下 capture 子句等效:
- [&, foo1]
- [=, &tot]
- [&tot, foo1]
- [foo1, &tot]
this 和标识符在 capture 中出现的次数不能超过一次。接下来的伪代码片段会提示一些内容(该部分参考微软提供的资料):
- struct S { void f(int i); };
-
- void S::f(int i) {
- [&, i]{}; // OK
- [=, *this]{ }; // OK: captures this by value. See below.
-
- [&, &i]{}; // ERROR: i preceded by & when & is the default
- [=, this]{}; // ERROR: this when = is the default
- [i, i]{}; // ERROR: i repeated
- }
下面这一部分代码和说明是参考C++官网资料:
- If the capture-default is &, subsequent simple captures must not begin with &:
-
- struct S2 { void f(int i); };
-
- void S2::f(int i)
- {
- [&] {}; // OK: by-reference capture default
- [&, i] {}; // OK: by-reference capture, except i is captured by copy
- [&, &i] {}; // Error: by-reference capture when by-reference is the default
- [&, this] {}; // OK, equivalent to [&]
- [&, this, i] {}; // OK, equivalent to [&, i]
- }
-
- // ...
-
- If the capture-default is =, subsequent simple captures must begin with & or be *this (since C++17) or this (since C++20):
-
- struct S2 { void f(int i); };
-
- void S2::f(int i) {
- [=] {}; // OK: by-copy capture default
- [=, &i] {}; // OK: by-copy capture, except i is captured by reference
- [=, *this] {}; // until C++17: Error: invalid syntax
- // since C++17: OK: captures the enclosing S2 by copy
- [=, this] {}; // until C++20: Error: this when = is the default
- // since C++20: OK, same as [=]
- }
-
- // ...
-
- Any capture may appear only once, and its name must be different from any parameter name:
-
- struct S2 { void f(int i); };
-
- void S2::f(int i)
- {
- [i, i] {}; // Error: i repeated
- [this, *this] {}; // Error: "this" repeated (C++17)
-
- [i] (int i) {}; // Error: parameter and capture have the same name
- }
当然还有些捕获后跟省略号的做法,这是一个包扩展,比如可变参数模板:
- template<class... Args>
-
- void foo(Args... args) {
- auto x = [args...] { return g(args...); };
- x();
- }
如果要访问对封闭类中的成员变量和成员函数,需要将 this 指针传递给 capture,这样将会获得访问的权限。
当采用多线程的 lambda 表达式时,需要注意几点:
a. 引用捕获会跟随外部变量的更新,但是值捕获是不会的
b. 引用捕获修改时,外部变量的值会更新,同样,值捕获是不会的。
c. 引用捕获会引入生存周期,而值捕获不会。
C++ 14中引入通用捕获,即可以在 capture 中引入和初始化新变量,无需将这些变量存于 lambda 的闭包范围之内。有个比较不错的特性,即初始化可以以任意表达式表示,并且可以根据生成的类型推导新变量的类型,可以平行理解为 auto 关键字。
- pString = make_unique
("Hello world!"); -
- auto pp = [p = move(pString)]()
- {
- // use p
- };
Lambda 表达式是可以接收输入参数的,同时参数列表在 Lambda 中是可选的,有点类似于函数接口中的参数列表。(下方伪代码只是示例)
- auto test = [] (uint_8 i, uint_8 j) {
- return i * j;
- }
C++ 14 可以使用 auto 关键字作为类型说明符,即当使用了 auto 关键字后编译器会将函数调用的运算符创建为模板,从而参数列表中定义为 auto 的实例都等效于对应的参数类型。
- auto test = [] (auto i, auto j) {
- return i * j;
- }
当你不进行传参操作时,你可以省略掉 "()",因为参数列表是可选择的,其它的可选项基本上是一个道理的。
- auto test = [] {
- return false;
- }
简单来说,利用mutable,Lambda 体可以修改通过值捕获的变量。
- #include
-
- int main() {
- int a = 1, b = 1, c = 1;
-
- auto foo1 = [a, &b, &c]() mutable {
- auto foo2 = [a, b, &c]() mutable {
- a = 2;
- b = 2;
- c = 2;
- };
- a = 3;
- b = 3;
- c = 3;
- foo2();
- };
- }
注意: mutable可以允许修改副本,而不能修改原始项。
在没有特别指定类型,如 trailing-return-type 的情况下,通常是无需 auto 关键字就可以自动推导 Lambda 表达式的返回类型。(trailing-return-type 实际上类似于普通函数接口的返回类型,只是它必须跟在参数列表的后面,且必须在返回类型前面包含 trailing-return-type 关键字 ->)
- auto test = [] (uint8_t i, uint8_t j) -> bool {
- return i > j;
- }
假设 Lambda 未提示返回值的情况下,如果包含有返回语句,则编译器将会自动推导出表达式的返回类型,即 return-type。否则,将会被推导为 void。
- // OK: return type is uint8_t
- auto test1 = [] (uint8_t i) {
- return i * i;
- };
-
- // ERROR: return type is void, deducing
- auto test2 = [] {
- return {1, 2, 3}; // return type from braced-init-list isn't valid
- };
跟大多数函数接口一样,Lambda 可以再生成 Lambda 来作为其返回值。类似于上述 Lambda mutable 中代码所体现的 foo1 和 foo2。
Lambda 体是一个复合语句,可以包含类似于函数接口中所允许的任意内容,比如一下的变量类型皆可访问:
a. 参数
b. 本地变量
c. 类成员参数,需要捕获 this
d. 当前封闭范围内所声明的变量
e. 全局变量
其实上述很多代码段已经展示了 Lambda 的主体,如果需要参考的话,基础的可以按照上面的结构来对比使用,这样可以减少很多问题。
- auto foo = [](int n)
- {
- std::function<int(int, int, int)> ff = [&](int n, int a, int b)
- {
- return n ? ff(n - 1, a + b, a) : b;
- };
- return ff(n, 0, 1);
- };
综上,我只是根据已有的一些资料结合自己的理解对 Lambda 表达式进行了一个基本的介绍。其中也有很多没有涉及到的部分,比如 noexcept 异常规范,constexpr 关键字对 Lambda 的修饰等,这些没有涉及到的部分有些是我还没来得及整理的,有些是我也没有遇到过的,所以暂时就不进行补充说明了。这篇文章主要着重于帮助大家理解最基本的 Lambda 结构和如何正确使用,这样可以减少代码问题的发生。
Lambda expressions (since C++11) - cppreference.com
上述链接里面有非常清楚的介绍,也包括一些高级的用法,有兴趣的小伙伴不妨试试去看看。