C++ lambda表达式的实际应用(附带实例)
C++ 重要的现代特性之一是 lambda 表达式,也称为 lambda 函数或 lambda。
lambda 表达式使我们能够定义匿名函数对象,这些对象可以捕获作用域中的变量,并且可以被调用或作为函数的参数传递。因此,lambda 在很多方面都很有用。
1) 如果只需要在单个位置使用 lambda,请在调用位置定义匿名 lambda 表达式:
2) 如果需要在多个位置调用 lambda,则定义命名 lambda,即赋值给变量的 lambda(通常使用类型的 auto 标识符):
3) 如果需要的 lambda 仅在参数类型方面不同,则使用泛型 lambda 表达式(从 C++14 起可用):
前面示例的 __lambda_name__函数对象实际上是编译器生成的内容的简化,因为它还定义了默认的复制构造函数和移动构造函数、默认的析构函数和已删除的赋值操作符。
这个 lambda 通过复制(即值传递)捕获两个变量:minimum 和 maximum。编译器创建的未命名函数对象与我们前面定义的函数对象非常相似。使用前面提到的默认特殊成员和已删除的特殊成员,该类看起来如下所示:
lambda 表达式可以通过复制(值传递)或引用捕获变量,也可以采用这两者的不同组合方式。但是,不能多次捕获一个变量,只能在捕获列表的开头使用 & 或 =。
下表展示了 lambda 捕获语义的各种组合:
从 C++17 开始,lambda 表达式的一般形式如下:
以下示例展示了广义 lambda 如何用 move 捕获变量:
在类方法中编写并需要捕获类数据成员的 lambda,可以通过以下几种方式实现:
1) 使用 [x=expr] 的形式捕获单个数据成员:
2) 使用 [=] 形式捕获整个对象(注意,通过 [=] 隐式捕获指针 this 在 C++20 中已弃用):
3) 通过捕获 this 指针来捕获整个对象,如果需要调用该类的其他方法,这是必需的。当指针被按值捕获时,可以将其捕获为 [this];当对象本身被按值捕获时,可以将其捕获为 [*this]。如果在捕获发生后但在调用 lambda 之前对象可能会超出作用域,那么这将产生很大的区别:
C++20 标准对捕获 this 进行了以下几项更改:
在某些情况下,lambda 表达式仅在参数方面有所不同。在这种情况下,lambda 可以像模板一样以泛型方式编写,但要使用类型参数的 auto 标识符(不涉及模板语法)。
lambda 表达式使我们能够定义匿名函数对象,这些对象可以捕获作用域中的变量,并且可以被调用或作为函数的参数传递。因此,lambda 在很多方面都很有用。
C++ lambda使用方式
使用 lambda 表达式而不是函数或函数对象来传递回调:1) 如果只需要在单个位置使用 lambda,请在调用位置定义匿名 lambda 表达式:
auto numbers = std::vector<int>{ 0, 2, -3, 5, -1, 6, 8, -4, 9 }; auto positives = std::count_if( std::begin(numbers), std::end(numbers), [](int const n) {return n > 0; });
2) 如果需要在多个位置调用 lambda,则定义命名 lambda,即赋值给变量的 lambda(通常使用类型的 auto 标识符):
auto ispositive = [](int const n) {return n > 0; }; auto positives = std::count_if( std::begin(numbers), std::end(numbers), ispositive);
3) 如果需要的 lambda 仅在参数类型方面不同,则使用泛型 lambda 表达式(从 C++14 起可用):
auto positives = std::count_if( std::begin(numbers), std::end(numbers), [](auto const n) {return n > 0; });
C++ lambda工作原理
第二个示例中显示的非泛型 lambda 表达式接受一个常量整数,如果它大于 0,则返回 true,否则返回 false。编译器用 operator 调用操作符定义一个未命名的函数对象,该对象为 lambda 表达式的签名:struct __lambda_name__ { bool operator()(int const n) const { return n > 0; } };编译器定义未命名函数对象的方式取决于我们定义 lambda 表达式的方式,该表达式可以捕获变量、使用 mutable 说明符或异常规范,或者具有尾部返回类型。
前面示例的 __lambda_name__函数对象实际上是编译器生成的内容的简化,因为它还定义了默认的复制构造函数和移动构造函数、默认的析构函数和已删除的赋值操作符。
在下面的示例中,我们要计算大于或等于 5 且小于或等于 10 的范围内的元素,lambda 表达式如下所示:lambda 表达式实际上是一个类,为了调用它,编译器需要实例化类的对象。从 lambda 表达式实例化的对象称为 lambda 闭包。
auto numbers = std::vector<int>{ 0, 2, -3, 5, -1, 6, 8, -4, 9 }; auto minimum { 5 }; auto maximum { 10 }; auto inrange = std::count_if( std::begin(numbers), std::end(numbers), [minimum, maximum](int const n) { return minimum <= n && n <= maximum;});
这个 lambda 通过复制(即值传递)捕获两个变量:minimum 和 maximum。编译器创建的未命名函数对象与我们前面定义的函数对象非常相似。使用前面提到的默认特殊成员和已删除的特殊成员,该类看起来如下所示:
class __lambda_name_2__ { int minimum_; int maximum_; public: explicit __lambda_name_2__(int const minimum, int const maximum) : minimum_( minimum), maximum_( maximum) {} __lambda_name_2__(const __lambda_name_2__&) = default; __lambda_name_2__(__lambda_name_2__&&) = default; __lambda_name_2__& operator=(const __lambda_name_2__&) = delete; ~__lambda_name_2__() = default; bool operator()( int const n) const { return minimum_ <= n && n <= maximum_; } };
lambda 表达式可以通过复制(值传递)或引用捕获变量,也可以采用这两者的不同组合方式。但是,不能多次捕获一个变量,只能在捕获列表的开头使用 & 或 =。
下表展示了 lambda 捕获语义的各种组合:
lambda | 描述 |
---|---|
[]() | 不捕获任何参数 |
[&]() | 以引用的形式捕获参数 |
[=]() | 以复制的形式捕获参数。指针 this 的隐式捕获在 C++20 中已弃用 |
[&x]() | 以引用的形式捕获 x |
[x]() | 以复制的形式捕获 x |
[&x, ...]() | 以引用的形式捕获包扩展 x |
[x, ...]() | 以复制的形式捕获包扩展 x |
[&, x]() | 以引用的形式捕获除 x 外的所有变量,但是以复制的形式捕获 x |
[=, &x]() | 以复制的形式捕获除 x 外的所有变量,但是以引用的形式捕获 x |
[&, this]() | 以引用的形式捕获除 this 外的所有变量,但是以复制的形式捕获 this |
[x, &]() | 错误,x 被捕获了两次 |
[&, &x]() | 错误,所有的参数都以引用的形式捕获,不能再次指定 x 以引用的形式捕获 |
[=, =x]() | 错误,所有的参数都以复制的形式捕获,不能再次指定 x 以复制的形式捕获 |
[this]() | 错误,this 指针总是以复制的形式捕获 |
从 C++17 开始,lambda 表达式的一般形式如下:
[capture-list](params) mutable constexpr exception attr -> ret { body }此语法中显示的所有部分实际上都是可选的,但捕获列表和主体除外,捕获列表可以为空,主体也可以为空:
- 如果不需要参数,则可以省略参数列表;
- 不需要指定返回类型,因为编译器可以从返回表达式的类型推导出它;
- mutable 说明符(指示编译器 lambda 实际上可以修改通过复制捕获的变量)、constexpr 说明符(指示编译器生成 constexpr 调用操作符)以及异常说明符和属性都是可选的。
上表中的后两个示例是广义 lambda 捕获形式,它们是在 C++14 中引入的,允许我们捕获仅支持移动语义的变量,但它们也可以用于在 lambda 中定义新的对象。最简单的 lambda 表达式是 [] {}, 但它通常写成 [] () {}。
以下示例展示了广义 lambda 如何用 move 捕获变量:
auto ptr = std::unique_ptr<int>(42); auto l = [ptr = std::move(ptr)](){return ++*lptr;};
在类方法中编写并需要捕获类数据成员的 lambda,可以通过以下几种方式实现:
1) 使用 [x=expr] 的形式捕获单个数据成员:
struct foo { int id; std::string name; auto run() { return [=] { std::cout << id << ' ' << n << '\n'; } };
2) 使用 [=] 形式捕获整个对象(注意,通过 [=] 隐式捕获指针 this 在 C++20 中已弃用):
struct foo { int id; std::string name; auto run() { return [=] { std::cout << id << ' ' << name << '\n'; } };
3) 通过捕获 this 指针来捕获整个对象,如果需要调用该类的其他方法,这是必需的。当指针被按值捕获时,可以将其捕获为 [this];当对象本身被按值捕获时,可以将其捕获为 [*this]。如果在捕获发生后但在调用 lambda 之前对象可能会超出作用域,那么这将产生很大的区别:
struct foo { int id; std::string name; auto run() { return[this] { std::cout << id << ' ' << name << '\n'; } }; auto l = foo{ 42, "john" }.run(); l(); // does not print 42 john在后一种情况下,正确的捕获应该是 [*this],以便按值复制对象。在本例中,调用 lambda 将打印 42john,即使临时变量已超出作用域。
C++20 标准对捕获 this 进行了以下几项更改:
- 它不赞成在使用 [=] 时隐式捕获 this,编译器会产生一个废弃警告;
- 当想用 [=, this] 捕获所有内容时,它引入了显式的 this 指针值捕获,我们仍然只能用 [this] 捕获指针 this。
在某些情况下,lambda 表达式仅在参数方面有所不同。在这种情况下,lambda 可以像模板一样以泛型方式编写,但要使用类型参数的 auto 标识符(不涉及模板语法)。