C++ 中的 lambda 表达式

lambda 表达式——《C++ Primer》笔记

介绍 lambda

首先回顾一下可调用对象的概念:我们说,如果对于一个对象或者一个表达式,可以对其使用调用运算符 () ,则称它为可调用对象。
可调用对象有这些:

  • 函数
  • 函数指针
  • 重载了调用运算符的类(也称为函数对象
  • lambda 表达式

那么什么是 lambda 表达式呢?lambda 表达式可以理解成一个未命名的内联函数。它具有返回类型、参数列表、函数体。形如这样:
[capture list] (parameter list) -> return type { function body }
capture list 也就是捕获列表,是一个 lambda 所在函数中定义的局部变量的列表(通常为空)。与普通函数所不同,lambda 表达式必须使用尾置返回类型
tip: 我们可以忽略参数列表和返回类型,但必须永远包含捕获列表(即使它为空)和函数体。

如果 lambda 的函数体只包含一个 return 语句,则返回类型可以从返回的表达式类型中推断出来。否则,返回类型为 **void**

    // 单一的 return 语句,推断返回类型为 int
    auto f = [] { return 42; };
    std::cout << f() << std::endl;

lambda 的原理浅讲

lambda 底层到底是怎么实现的呢?
当我们定义一个 lambda 时,编译器就会生成一个与 lambda 对应的新的(未命名的)类类型。(后面再会补充细讲)
默认情况下,从 lambda 生成的类都包含一个对应该 lambda 所捕获的变量的数据成员。lambda 的数据成员在 lambda 对象创建时被初始化。

lambda 捕获

lambda 捕获方式有两种:值捕获引用捕获

值捕获

跟普通函数传递参数类似,lambda 使用值捕获的前提是变量可以拷贝。由我们上文讲到的 lambda 实现方式,我们可以知道,被捕获的变量的值在 lambda 创建时拷贝,而不是调用时拷贝。
举个代码例子:

void fcn1() {
    size_t v1 = 42;
    auto f = [v1] { return v1; };
    v1 = 0;
    auto j = f();   // j 为42
}

由于被捕获变量的值在 lambda 创建时拷贝,因此随后对其修改不会影响到 lambda 内对应的值。

引用捕获

我们定义 lambda 表达式时可以采用引用方式来捕获变量。

void fcn2() {
    size_t v1 = 42;
    auto f2 = [&v1] { return v1; };
    v1 = 0;
    auto j = f2();  // j 为 0
}

一个特别要注意的点是:当以引用方式捕获一个变量时,必须保证在 lambda 执行时变量是存在的。
由于这个原因,当我们使用一个函数返回一个 lambda 时,此 lambda 不能包含引用捕获。

捕获的一些注意点

一般来说,我们应该尽量减少捕获的数据量,来避免潜在的捕获导致的问题。而且,如果可能的话,应该避免捕获指针或引用。
原因在于:如果我们捕获一个指针或迭代器,或采用引用捕获方式,就必须确保在 lambda 执行时,绑定到迭代器、指针或者引用的对象仍然存在,而且需要保证对象具有预期的值。

void fcn3() {
    int *a = new int(10);
    auto f = [a] { return *a; };
    *a = 10;
    auto j = f();   // 预期中 a 应该是10,现在却是0;
}

void fcn4() {
    int  *a = new int(10);
    auto f = [a] { return *a; };
    delete a;
    auto j = f();   // 错误:a指向的对象已经不存在了
}

隐式捕获

我们可以让编译器根据 lambda 函数体中的代码来推断我们要使用哪些变量。这时候我们要在捕获列表中写一个&或者=

  • & 告诉编译器采用引用捕获方式
  • = 告诉编译器采用值捕获方式
void fcn5() {
    int v1 = 42;
    // 隐式捕获 v1,捕获方式为值捕获
    auto f = [=] { return v1; };
    auto j = f();
}

更加有意思的是,我们还可以混用隐式捕获和显式捕获。但是我们必须接受一些限制:

  • 捕获列表中的第一个元素必须是一个&或者=
  • 显式捕获的变量必须使用与隐式捕获不同的捕获方式。也就是如果隐式捕获是引用捕获,那么显式捕获就必须是值捕获;如果隐式捕获是值捕获,那么显式捕获必须是引用捕获。
void fcn6() {
    std::ostream &os = std::cout;
    char c = ' ';
    // os 为隐式捕获, c 为显式捕获
    auto f = [&, c] (const string &str) { os << str << c; };
    // c 为隐式捕获,os 为隐式捕获
    auto g = [=, &os] (const string &str) { os << str << c; };

    f("Hello,");
    g("World!");
}

可变 lambda

默认情况下,对于一个被值拷贝的变量,lambda 不会改变它的值。如果我们希望能改变一个被捕获变量的值,就必须在参数列表首加上关键字 **mutable**。因此,可变 lambda 不能忽略参数列表(但可以为空)

void fcn7() {
    size_t v1 = 42;
    auto f = [v1] () mutable { return ++v1; };
    v1 = 0;
    auto j = f(); // j 为43
}

一个引用捕获的变量能否修改依赖于此引用指向的是一个 const 类型还是一个非 const 类型:

void fcn8() {
    size_t v1 = 42;
    auto f2 = [&v1] { return ++v1; };
    v1 = 0;
    auto j = f2();  // j 为1
}

lambda 返回类型

默认情况下,如果一个 lambda 函数体内包含 return 之外的任何语句,则编译器假定此 lambda 返回void,被推断返回void的 lambda 不能返回值。
当我们需要为一个 lambda 定义返回类型时,必须使用尾置返回类型

void fcn9() {
    auto f = [] (int i) -> int
            {   if (i < 0) return -i;
                else return i;
            };

    auto j = f(-1); // j 为1
}