2. C++11 中的 Lambda


万岁!C++ 委员会听取了开发人员的意见,在 C++11 标准中加入了 lambda 表达式!

Lambda 表达式很快就成为现代 C++ 中最具辨识度的一个特性。

你可以在 N3337(C++11 的最终草案)中阅读其完整规范,以及关于 lambda 的单独部分:[express .prim.lambda]。

我认为委员会以一种聪明的方式在语言中添加了 lambda。他们设计了新的语法,但随后编译器将其“展开”为一个未命名的“隐藏的”函数对象类型。这样我们就拥有了真正强类型语言的所有优点(以及缺点),使代码理解起来更加容易。

在本章,你将会学习到:

  • Lambda 的基础语法。
  • 如何捕获一个变量。
  • 如何捕获一个类的非静态成员变量。
  • Lambda 的返回类型。
  • 什么是闭包类型。
  • 怎样将 lambda 表达式转换成一个函数指针从而能够去使用 C 风格的 API.
  • 什么是 IIFE 以及为什么它是的有用的。
  • 如何继承一个 lambda 表达式。

让我们出发吧!


Lambda 表达式的语法

下图说明了 C++11 中 lambda 的语法:

现在让我们通过几个例子来感受一下它。


Lambda 表达式的几个例子

// 1. 最简单的 lambda 表达式:
[] {};

在第一个示例中,你可以看到一个“最迷你”的 lambda 表达式。它只需要[]部分
(lambda 引入器),然后用空的{}部分作为函数体。形参列表()是可选的,在本例中不需要。

// 2. 拥有两个参数的 lambda:
[] (float f, int a) { return a * f; };
[] (int a, int b) { return a < b; };

在第二个例子中,可能是最常见的例子了,你可以看到参数都传递到()部分,就像普通函数一样。返回类型不需要,因为编译器会自动推导它。

// 3. 尾置返回类型:
[] (MyClass t) -> int { auto a = t.compute(); print(a); return a; };

在上面的例子中,我们显式地设置了一个返回类型。后面的返回类型也可用在 C++11 以来的常规函数声明中。

// 4. 额外的说明符:
[x] (int a, int b) mutable { ++x; return a < b; };
[] (float param) noexcept { return param * param; };
[x] (int a, int b) mutable noexcept { ++x; return a < b; };

最后一个示例显示,在 lambda 的主体之前,可以使用其他说明符。在代码中,我们使用了 mutable(这样我们可以改变捕获的变量)和noexcept。第三个 lambda 使用了mutablenoexcept,并且它们必须以该顺序出现(你不能写noexcept mutable,因为编译器会拒绝它)。

虽然()部分是可选的,但如果你想应用 mutablenoexcept,此时()则需要在出现的表达中:

// 5. 可选项
[x] { std::cout << x; }; // 不需要 () 
[x] mutable { ++x; }; // 无法通过编译!
[x] () mutable { ++x; }; // 可以,mutable 前面的 () 是必要的
[] noexcept { }; // 无法通过编译!
[] () noexcept { }; // 可以

同样的模式也适用于其他可以应用于 lambdas 的说明符,比如 C++17 中的 constexpr 和 C++20 中的 consteval

在熟悉了基本的例子之后,我们现在可以尝试去理解它是如何工作的,并学习 lambda 表达式的所有可能用法。


核心定义

在我们继续之前,从 C++ 标准中引入一些核心定义是很方便的:
来自 [expr.prim.lambda#2]

lambda 表达式的计算结果是一个临时的纯右值。这个临时值叫做闭包对象。

作为旁注,lambda 表达式是一个 prvalue 即“纯右值” 。这种类型的表达式通常产生自初始化并出现在赋值的右侧(或在 return 语句中)。阅读 C++ Reference,[express .prim.lambda#3] 中给出的的另一个定义是:

lambda 表达式的类型(也就是闭包对象的类型)是一个唯一的,未命名的非联合类类型——称为闭包类型。


编译器展开

从以上定义中,我们可以了解到编译器从一个 lambda 表达式生成唯一的闭包类型。然后我们可以通过这个类型来实例化出闭包对象。

以下示例展示了如何写一个 lambda 表达式并将其传给std::for_each。为了便于比较,代码还说明了编译器生成的相应的函数对象类型:

// Ex2_1: Lambda 和 相应的函数对象。
#include 
#include 
#include 

int main() {
    struct {
        void operator()(int x) const {
            std::cout << x << '\n';
        }
    } someInstance;

    const std::vector v { 1, 2, 3 };
    std::for_each(v.cbegin(), v.cend(), someInstance);
    std::for_each(v.cbegin(), v.cend(), [] (int x) {
            std::cout << x << '\n';
        }
    );
}

在本例中,编译器将
[](int x) { std::cout << x << '\n'; }
翻译成一个匿名函数对象,简化形式如下:

struct {
    void operator()(int x) const {
        std::cout << x << '\n';
    }
} someInstance;

“翻译”或“展开”的过程可以很容易地在 C++ Insights 在线网页工具上看到。该工具获取有效的 C++ 代码,然后产生编译器需要的源代码版本:像 lambda 的匿名函数对象,模板的实例化等其他 C++ 的特性。

在下一节中,我们将深入研究 lambda 表达式的各个部分。


Lambda 表达式的类型

由于编译器为每个 lambda (闭包类型)生成唯一的名称,所以我们就没法把它“拼写”在前面。

这就是为什么必须使用auto(或 decltype)来推断其类型。
auto myLambda = [](int a) -> double { return 2.0 * a; };
而且,如果你有两个看起来一样的 lambda:
auto firstLam = [](int x) { return x * 2; };
auto secondLam = [](int x) { return x * 2; };
它们的类型也是不同的,即使“代码背后”是相同的!编译器需要为这两个 lambda 声明的每个都生成惟一的匿名类型。我们可以用下面的代码来证明这个属性:

// Ex2_1: 相同的代码,不同的类型。
#include 

int main() {
    const auto oneLam = [](int x) noexcept { return x * 2; };
    const auto twoLam = [](int x) noexcept { return x * 2; };
    static_assert(!std::is_same::value,
                  "must be different!");
}

上面的例子验证了 oneLam 和 twoLam 的闭包类型是否不相同。

在 C++17 中我们可以使用无需消息的static_assert以及用于类型萃取的辅助变量模板is_same_vstatic_assert(std::is_same_v);

然而,虽然你不知道确切的名称,但是你还是可以拼出 lambda 的签名,然后将其存储在std::function中。一般来说,如果 lambda 是通过 std::function<>类型“表示”的,那么它可以完成定义为auto的 lambda 无法完成的任务。例如,前面的 lambda 具有double(int)的签名,因为它接受int作为输入参数并返回double。然后我们可以用以下方法创建std::function对象:

std::function myFunc = [](int a) -> double { return 2.0 * a; };

std::function 是一个重量级的对象,因为它需要处理所有可调用对象。要做到这一点,它需要高级的内部机制,如类型双关语,甚至是动态内存分配。我们可以通过一个简单的实验来检验它的大小:

// Ex2_3: std::function 和 auto 类型推导。
#include 
#include 

int main() {
    const auto myLambda = [](int a) noexcept -> double {
        return 2.0 * a;
    };

    const std::function myFunc =
        [](int a) noexcept -> double {
        return 2.0 * a;
    };

    std::cout << "sizeof(myLambda) is " << sizeof(myLambda) << '\n';
    std::cout << "sizeof(myFunc) is " << sizeof(myFunc) << '\n';

    return myLambda(10) == myFunc(10);
}

在 GCC 编译下代码输出如下:
sizeof(myLambda) is 1
sizeof(myFunc) is 32
因为 myLambda 只是一个无状态的 lambda,所以它也是一个空类,没有任何数据成员字段,所以它的最小大小只有一个字节。另一边的std::function版本则要大得多——32 个字节。这就是为什么如果可以的话,应该依靠自动类型推导来获得尽可能小的闭包对象。

当我们讨论std::function时,还需要注意的是,这种类型不是只移型闭包。你可以在 C++14 的可移动的类型章节中阅读关于这个问题的更多信息。

你可能感兴趣的:(2. C++11 中的 Lambda)