解密C++ lambdas(译)

Lambda是C ++中新增的功能之一,似乎在许多程序员中引起了极大的震惊。在本文中,我们将研究lambda的语法和底层实现,以尝试将它们放入某种上下文中。

Functors and the Standard Library

使用STL算法,对每个元素的处理由用户提供的一元或二元函数对象执行。对于常用的操作时,可以使用 stl 提供的functor(例如 std: : divisions),但对于定制操作,必须创建定制函数或functors。

图片

仿函数是提供operaator()实现的特殊类 。

对于与STL算法一起使用的仿函数,operator()函数必须采用一个(对于一元过程)或两个(对于二进制过程)适当类型的参数。

创建定制仿函数可能会很费力。尤其是如果仿函数仅在一个特定的地方使用。这些定制仿函数可能会不经意地“弄乱了”代码。

引入lambda

Lambda是一种临时的,局部作用域的函数(严格来说,是一个仿函数)。基本上,lambda是语法糖,旨在减少创建临时仿函数类所需的大量工作。

图片

方括号([])标记了lambda的声明;它可以有参数,并且后面应跟其主体(与任何其他函数相同)。

执行lambda时,将使用标准ABI机制传递参数。Lambda和函数之间的区别:Lambda参数不能具有默认值。

Lambda的主体只是一个普通的函数体,并且可以任意复杂(尽管,如我们所见,保持Lambda的主体相对简单是一种很好的做法)。

注意,lambda使用尾随返回类型声明。(毫无疑问)这是为了简化解析(因为类型不是有效的函数参数)。

在以下情况下,可以省略返回类型:

  • 返回类型为空
  • 编译器可以推断出返回类型(lambda主体为return <type>

Lambda是一个对象(因此为什么我们将其称为仿函数而不是函数),因此具有类型并且可以存储。但是,lambda的类型仅由编译器知道(因为它是编译器生成的),所以必须对 lambda 的声明实例使用 auto。

图片

Lambdas 允许在块作用域声明临时函数(在 c + + 中以前是非法的)。在此示例中,lambda函数(f​​unctor)仅在func()范围内可用。与具有全局作用域(或文件作用域,如果声明为static)的函数不同。

这意味着我可以用lambda替换STL算法中的手动创建的仿函数:

图片

在上面的代码中,for_each算法会使容器中的每个元素都调用lambda。

所以,这很整洁。但是它真正给我带来了什么呢?

内联Lambda

由于lambda是作用域函数,因此我们可以在实际需要的范围内定义它-在本例中,是在for_each算法中。

图片

现在,lambda 在算法体中定义,并且实际上只在算法的生命周期中存在。 我们已经将代码的范围缩小到恰好需要它的地方(这是好的模块化设计的原则之一)。

从可读性的角度来看,我们已经将功能正确地放在了使用位置。与functor不同,它很可能在另一个模块中定义(程序员必须去查找和/或从上下文中了解使用它的位置)。

深入1

定义lambda时,编译器会使用该lambda创建一个临时functor class。名称是由编译器生成的(可能不是人类可读的)

图片

lambda主体用于在functor类上生成operator()方法。客户端代码被修改为使用新的 lambda-functor。

图片

捕捉上下文

有时,能够从lambda的包含范围(即lambda的定义范围)中访问对象既有用又方便。我们可以将它们作为参数传递给lambda(就像普通函数一样);但是,这不适用于算法,因为该算法没有从代码中传递额外参数的机制(怎么可能?)

如果您正在编写自己的functor,则可以通过将适当的参数传递给functor的构造函数来实现。C + + 提供了一种方便的机制来实现这一点,这种机制称为“捕获上下文”。

Lambda 的上下文是调用 lambda 时范围内的一组对象。可以捕获上下文对象,然后将其用作lambda处理的一部分。

通过名称捕获对象将生成该对象的lambda局部副本。

图片

通过引用捕获对象允许lambda操纵其上下文。也就是说,lambda可以更改已通过引用捕获的对象的值。

图片

这里要提一个警告:如我们所见,lambda只是一个对象,就像其他对象一样,它可以被复制,作为参数传递,存储在容器中,等等。Lambda 对象有自己的作用域和生存期,在某些情况下,这些作用域和生存期可能与它所“捕获”的对象不同。 在通过引用捕获本地对象时要非常小心,因为 lambda 的生存期可能超过其捕获列表的生存期。 换句话说,lambda 可能对范围中不再存在的对象有一个引用!

作用域中的所有变量都可以使用 default-capture 来捕获。 这使得当前作用域中的所有自动变量都可用。

图片

注意,只有在lambda主体中实际使用了捕获的对象的情况下,编译器才会复制这些对象。

深入2

当向 lambda 添加捕获列表时,编译器会向 lambda-functor 类添加适当的成员变量,并为这些变量初始化添加一个构造函数。

图片

现在比较容易理解为什么捕获上下文会有潜在的开销: 对于每个通过值捕获的对象,都会创建一个原始对象的副本; 对于每个通过引用捕获的对象,都会存储一个引用。

图片

成员函数中的Lambda

我们很可能(而且很可能)希望在类成员函数中使用lambda。请记住,lambda是其自身的唯一(且独立)类,因此执行时会有自己的上下文。因此,它不能直接访问该类的任何成员变量。

为了捕获类的成员变量,我们必须捕获类的这个指针。 我们现在可以完全访问所有类的数据(包括私有数据,因为我们在一个成员函数中)。

图片

可调用对象

可调用对象是可以像函数一样调用的任何对象的通用名称:

  • A member function (pointer)
  • A free function (pointer)
  • A functor
  • A lambda

在C语言中有函数指针(pointer-to-function)的概念,该函数允许存储任何函数的地址(假设其签名与指针的签名匹配)。但是,pointer-function与pointer-to-member-function的签名不同,对lambda来说就是不同签名。最好是一个通用的“指向可调用对象的指针(pointer-to-callable-object)”,它可以存储任何可调用对象的地址(当然要提供其签名匹配的对象)。

std :: function是一个模板类,可以容纳与其签名匹配的任何可调用对象。它为存储、传递和访问这些对象提供了一致的机制。

图片

如果可调用对象与std :: function的签名匹配,则可以将std :: function视为可以指向任何可调用对象的通用函数指针。而且,与C的函数指针不同,C ++编译器对可调用对象的参数(包括返回类型)进行了强大的类型检查。

图片

std :: function为operator!=提供了一个重载,以使其可以与nullptr进行比较(因此它可以像一个函数指针一样工作)。

我们的 SimpleCallback 类可以与任何可调用类型的functor、free functions 或者 lambdas 一起使用,而不需要做任何更改,因为它们都与回调所需的签名相匹配。

图片

综上所述

尽管语法上有些笨拙,但是 lambdas 并不是一种值得害怕或鄙视的机制。 它们仅仅提供了一种简化代码和减少程序员工作量的有用方法。 从本质上讲,它们只不过是语法糖。 在这方面,他们并不比运算符重载更有害。

转自:https://blog.feabhas.com/2014/03/demystifying-c-lambdas/

Show Comments