11.2 Lambda表达式

lambda表达式

我们可以向一个算法传递任何类型的可调用对象callable object,到目前为止我们仅使用过两种可调用对象:函数和函数指针。还包括其他两种可调用对象:重载了函数调用运算符的类和lambda表达式。

一个lambda表达式表示一个可调用的代码单元,我们将其理解为一个未命名的内联函数,具有返回类型、一个函数列表和一个函数体:

[capture list](parameter list) -> return type { function body }

0. 使用场景

对于那些只在一两个地方使用的简单操作,lambda表达式是最有用的。如果我们需要在很多地方使用相同的操作或者一个操作需要很多语句才能完成,那么通常使用函数更好。

1. 最简单的lambda表达式

我们可以忽略参数列表和返回类型,但必须包括捕获列表和函数体,我们定义一个可调用对象f,它不接受参数直接返回42

auto f = [] { return 42; };
std::cout << f() << std::endl;  // 打印42

2. 捕获列表

Tips:注意我们只对lambda表达式所在函数中定义的(非static)变量使用捕获列表。一个lambda表达式是可以直接使用局部static变量和它所在函数之外声明的名字。

我们可以构造一个按长度排序,长度相同的单词维持字典序,空捕获列表表示此lambda不使用它所在函数中的任何局部变量。

#include <iostream>
#include <algorithm>
#include <vector>
#include <string>

int main(void) {
    std::vector<std::string> words = {"tomo", "cat", "tomocat", "aaa", "bbbb", "ccccccc"};

    // stable_sort稳定排序算法: 维持相等元素的原有顺序
    stable_sort(words.begin(), words.end(),
        [] (const std::string &a, const std::string &b) {
            return a.size() < b.size();
        });

    for (auto const& word : words) {
        std::cout << word << std::endl;
    }
}

// 输出:
cat
aaa
tomo
bbbb
tomocat
ccccccc

我们将lambda放在一个函数内,通过捕获列表获取函数中的局部变量,例如我们可以查找第一个长度大于等于sz的元素:

#include <iostream>
#include <algorithm>
#include <vector>
#include <string>

int main(void) {
    std::vector<std::string> words = {"tomo", "cat", "tomocat", "aaa", "bbbb", "ccccccc"};
    const int sz = 5;

    // 获取一个迭代器, 指向第一个满足size() >= sz的元素, 如果不存在则返回尾后迭代器
    auto wc = find_if(words.begin(), words.end(),
        [sz](const std::string &a) {  // 捕获了lambda所在函数的非static变量sz, 值传递
            return a.size() >= sz;
        });

    if (wc != words.end()) {
        std::cout << *wc << std::endl;
    }
}

// 输出:
tomocat

lambda捕获和返回

编码规范:不要使用默认lambda捕获,所有捕获都要显式写出来。

// 不规范
[=](int x){ return x + n; }

// 规范
[n](int x){return x + n;}

Tips:当定义一个lambda表达式时,编译器同时生成了与该lambda对应的未命名的类类型。当向一个函数传入lambda时,相当于同时定义一个新类型和该类型的一个对象。默认情况下从lambda生成的类都包含对应该lambda所捕获的变量的数据成员。

1. 值捕获

注意lambda的值捕获具有如下两个特点:

  • 采用值捕获的前提是变量可以拷贝

  • 被捕获的变量是在创建时拷贝,而不是调用时拷贝

int main(void) {
    // 局部变量
    std::string str = "tomocat";
    // 值捕获: 将str拷贝到名为f的可调用对象
    auto f = [str] { return str; };
    // 修改str值
    str = "change";

    // 输出"tomocat", 即f保存了我们创建它时str的拷贝
    std::cout << f() << std::endl;
}

2. 引用捕获

Tips:当以引用方式捕获一个变量时,必须保证在lambda执行时变量是存在的。而且如果可能的话应尽量避免捕获指针或者引用。

我们也可以将上述的程序修改成引用捕获:

int main(void) {
    // 局部变量
    std::string str = "tomocat";
    // 引用捕获: f中包含str的引用
    auto f = [&str] { return str; };
    // 修改str值
    str = "change";

    // 输出"change"
    std::cout << f() << std::endl;
}

3. 隐式捕获

除了显式列出我们希望使用所在函数的变量外,还可以让编译器根据lambda体中的代码来推断我们要使用哪种变量。为了指示编译器推断捕获列表,应在捕获列表中写一个&或者=,前者表示引用捕获,后者表示值捕获。如果我们希望对一部分变量采用值捕获,对其他变量采用引用捕获,可以混合使用隐式捕获和显式捕获:

混合使用隐式捕获和显式捕获时,请注意:

  • 捕获列表第一个元素必须是一个=或者&,指定了默认捕获方式为引用或值捕获

  • 显式捕获的变量必须使用与隐式捕获不同的方式,即如果隐式捕获是引用方式则显式捕获必须采用值方式,反之亦成立

#include <iostream>
#include <algorithm>
#include <vector>
#include <string>

using std::vector;
using std::string;
using std::ostream;

void biggies(vector<string> &words, ostream &os = std::cout, char c = ' ') {
    // os隐式引用捕获, c显式值捕获
    std::cout << "####################################" << std::endl;
    for_each(words.begin(), words.end(),
        [&, c](const string &s) {os << s << c;});
    std::cout << std::endl;
    // os显式引用捕获, c隐式值捕获
    std::cout << "####################################" << std::endl;
    for_each(words.begin(), words.end(),
        [=, &os](const string &s) {os << s<< c;});
    std::cout << std::endl;
}

int main(void) {
    std::vector<std::string> words = {"tomo", "cat", "tomocat", "aaa", "bbbb", "ccccccc"};
    biggies(words);
}

// 输出:
####################################
tomo cat tomocat aaa bbbb ccccccc 
####################################
tomo cat tomocat aaa bbbb ccccccc 

4. 可变lambda

默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。如果我们希望能改变一个被捕获的变量的值,就必须在参数列表首加上关键字mutable

如果不加上mutable关键字的话,会报错:

src/main/main.cpp: In lambda function:
src/main/main.cpp:5:32: error: increment of read-only variable ‘i’
  auto f = [i] () { return ++i; };
#include <iostream>

int main(void) {
    int i = 10;
    auto f = [i] () mutable { return ++i; };
    i = 0;
    std::cout << f() << std::endl;
}

// 输出
11

5. 指定lambda返回类型

当我们需要为一个lambda定义返回类型时,需要使用尾置返回类型:

#include <string>
#include <vector>
#include <algorithm>

using std::vector;

int main(void) {
    vector<int> vi = {0, -1, 2, -3, 4, -5};

    // transform算法将一个序列中的每个负数都替换为绝对值
    transform(vi.begin(), vi.end(), vi.begin(),
        [](int i) -> int {
            if (i < 0) {
                return -i;
            } else {
                return i;
            }
        });

    for (int i : vi) {
        std::cout << i << std::endl;
    }
}

// 输出
0
1
2
3
4
5

lambda本质:函数对象

当我们编写了一个lambda后,编译器将其翻译成一个重载了函数调用运算符的未命名类的未命名对象。

1. 可变与不可变

默认情况下lambda不能改变它捕获的变量,因此在默认情况下由lambda产生的类当中的调用运算符是一个const成员函数,如果lambda被声明为可变的,那么调用运算符就不是const的了。

2. 引用捕获与值捕获

当一个lambda表达式通过引用捕获变量时,将由程序负责确保lambda执行时引用的对象却是存在,因此编译器可以直接使用该引用而无需在lambda产生的类中将其存储为数据成员。如果通过值捕获的变量被拷贝到lambda中,因此这种lambda产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其使用捕获的变量来初始化数据成员。