将lambda与标准算法配合使用

在C++中,类具有特殊的成员(构造函数,析构函数和运算符),这些成员可以由编译器默认实现,也可以由开发人员提供。但是,可以默认实现的规则有些复杂,可能会导致问题。另一方面,开发人员有时希望防止以特定方式复制,移动或构造对象。当然可以通过使用这些成员来实现不同的技巧。 C++ 11标准简化了许多这样的工作,它允许删除或默认设置功能,具体方法将在下一部分中看到。

做好准备

在本文中,我们将讨论标准算法,这些算法需要一个参数,这种参数是一个函数或是应用于迭代所有元素的谓词。您需要知道什么是一元和二元函数以及什么是谓词和比较函数。您还需要熟悉函数对象,因为lambda表达式是函数对象的语法糖。

怎么做……

您应该更偏向使用lambda表达式将回调传递给标准算法,而非函数或函数对象:

  • 如果只需要在单个位置使用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; });
  • 如果需要在多个位置调用lambda,请定义一个命名的lambda,即分配给一个变量的lambda(通常使用类型的自动说明符):
auto ispositive = [](int const n) {return n > 0; }; 
auto positives = std::count_if( 
  std::begin(numbers), std::end(numbers), ispositive);
  • 如果需要仅在参数类型上有所不同的lambda(自C++ 14起可用),请使用通用lambda表达式:
auto positives = std::count_if( 
  std::begin(numbers), std::end(numbers),  
  [](auto const n) {return n > 0; });

这个如何起作用……

前面第二个项目符号中显示的非泛型lambda表达式采用一个常量整数,如果它大于0,则返回true,否则返回false。 编译器使用调用操作符定义一个未命名的函数对象,该操作符具有lambda表达式的签名:

struct __lambda_name__ 
{ 
  bool operator()(int const n) const { return n > 0; } 
};

编译器定义未命名函数对象的方式取决于我们定义可以捕获变量,使用可变说明符或异常说明或具有尾随返回类型的lambda表达式的方式。 前面显示的lambda_name函数对象实际上是编译器生成内容的简化,因为它还定义了默认的复制和移动构造函数,默认的析构函数以及已删除的赋值运算符。

必须充分理解的是,lambda表达式实际上是一个类。 为了调用它,编译器需要实例化该类的对象。 从lambda表达式实例化的对象称为lambda闭包。

在下一个示例中,我们要计算大于或等于5且小于或等于10的范围内的元素数。在这种情况下,lambda表达式将如下所示:

auto numbers = std::vector<int>{ 0, 2, -3, 5, -1, 6, 8, -4, 9 }; 
auto start{ 5 }; 
auto end{ 10 }; 
auto inrange = std::count_if( 
         std::begin(numbers), std::end(numbers),  
         [start, end](int const n) {
            return start <= n && n <= end;});

此lambda通过副本(也就是实际值)捕获两个变量:开始和结束。由编译器创建的结果未命名函数对象非常类似于我们前面定义的对象。使用前面提到的默认成员和删除的特殊成员,该类如下所示:

class __lambda_name_2__ 
{ 
  int start_; 
  int end_; 
public: 
  explicit __lambda_name_2__(int const start, int const end) : 
    start_(start), end_(end) 
  {} 

  __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 start_ <= n && n <= end_; 
  } 
};

lambda表达式可以通过副本(也就是实际值)或引用来捕获变量,并且两者的不同组合是可能的。 但是,不可能多次捕获变量,并且只能在捕获列表的开头使用&或=。

Lambda只能从封闭函数作用域捕获变量。它无法捕获具有静态存储持续时间的变量(即,在命名空间范围内或使用静态或外部说明符声明的变量)。

下表显示了lambda捕获语义的各种组合:

Lambda描述
[](){} 不捕获任何东西
[&](){} 通过引用捕获所有内容
[=](){} 通过副本捕获所有内容
[&x](){} 通过引用仅捕获x
[x](){} 通过副本仅捕获x
[&x…](){} 通过引用捕获包扩展名x
[x…](){} 通过副本捕获扩展包x
[&, x](){} 通过引用捕获所有内容,但副本捕获的x除外
[=, &x](){} 通过副本捕获所有内容,但通过引用捕获的x除外
[&, this](){} 通过引用捕获所有内容,但指针捕获的则是副本捕获的内容(始终由副本捕获)
[x, x](){} 错误,x被捕获两次
[&, &x](){} 错误,所有内容均被引用捕获,无法再次指定以引用方式捕获x
[=, =x](){} 错误,所有内容均被 副本 捕获,无法再次指定以副本形式捕获x
[&this](){} 错误,指针始终被副本捕获
[&, =](){} 错误,无法通过副本和引用捕获所有内容

从C++ 17开始,lambda表达式的一般形式如下所示:

[capture-list](params) mutable constexpr exception attr -> ret
{ body }

实际上,此语法中显示的所有部分都是可选的,除了捕获列表(可以为空)和主体(也可以为空)。如果不需要参数,则实际上可以省略参数列表。不需要指定返回类型,因为编译器可以从返回表达式的类型中推断出它。可变说明符(告诉编译器lambda实际上可以修改通过副本捕获的变量),constexpr说明符(告诉编译器生成constexpr调用运算符),以及异常说明符和属性都是可选的。

最简单的lambda表达式是[] {},尽管通常将其写为[](){}。

还有更多…

在某些情况下,lambda表达式仅在其参数类型上有所不同。在这种情况下,可以像模板一样以通用方式编写lambda,但可以使用自动说明符作为类型参数(不涉及模板语法)。在“参见”部分中提到的下一个配方中将解决此问题。

参见

  • 使用通用Lambda
  • 编写递归lambda

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据