Kodewick
第 22 课,共 30 课目录

lambda 表达式

第 22 课,共 30 课 · 第 5 部分 · 内存和 lambda · 约 15 分钟

第 12 课降序排序写的是 std::greater<int>(),第 14 课按成员排序要在上面另写一个比较函数。其实可以在调用 sort 的地方,就地写一个小函数,这叫 lambda 表达式:

main.cpp
1#include <algorithm>2#include <iostream>3#include <vector>4 5int main() {6    std::vector<int> scores{72, 95, 88, 60};7    std::sort(scores.begin(), scores.end(), [](int a, int b) { return a > b; });8    for (int s : scores) {9        std::cout << s << " ";10    }11    std::cout << '\n';12    return 0;13}
输出
95 88 72 60 

[](int a, int b) { return a > b; } 就是一个没有名字的函数:两个参数,a 比 b 大就返回 true,意思是「大的排前面」。sort 每比较两个元素,就调用它一次。

lambda 的样子

一个 lambda 有三部分:方括号 [](捕获,下面讲)、圆括号里的参数、花括号里的函数体。返回类型不用写,编译器从 return 推出来。

lambda 也可以存进变量,像函数一样调用。它的类型只有编译器知道,所以用 auto:

main.cpp
1#include <iostream>2 3int main() {4    auto with_tax = [](double price) { return price * 1.1; };5    auto is_even = [](int n) { return n % 2 == 0; };6    std::cout << with_tax(50) << '\n';7    std::cout << with_tax(200) << '\n';8    std::cout << is_even(7) << '\n';9    return 0;10}
输出
55
220
0

点单步:每次调用 with_tax,画面上和普通函数一样多出一层,price 是这一次的值。

给 sort 用

按成员排序,以前要另写一个函数,现在比较的规矩就写在 sort 旁边,读的人不用往上翻:

main.cpp
1#include <algorithm>2#include <iostream>3#include <string>4#include <vector>5 6struct Student {7    std::string name;8    int score;9};10 11int main() {12    std::vector<Student> list{{"小林", 82}, {"小周", 95}, {"小陈", 76}};13    std::sort(list.begin(), list.end(), [](const Student& a, const Student& b) {14        return a.score > b.score;15    });16    for (const auto& s : list) {17        std::cout << s.name << " " << s.score << '\n';18    }19    return 0;20}
输出
小周 95
小林 82
小陈 76

参数写 const Student&,不复制、也不改(第 10 课)。lambda 的函数体可以有好几行,换行写,和普通函数一样。

count_if 和 find_if

<algorithm> 里很多函数名字带 _if,要的都是一个「问一个元素,回答 true 或 false」的函数。std::count_if 数有几个回答 true;std::find_if 找第一个回答 true 的,返回指着它的迭代器,找不到就是 end():

main.cpp
1#include <algorithm>2#include <iostream>3#include <vector>4 5int main() {6    std::vector<int> scores{72, 58, 91, 45, 66};7    auto failed = std::count_if(scores.begin(), scores.end(), [](int s) { return s < 60; });8    std::cout << "不及格:" << failed << " 人\n";9    auto it = std::find_if(scores.begin(), scores.end(), [](int s) { return s > 90; });10    if (it != scores.end()) {11        std::cout << "第一个超过 90 的:" << *it << '\n';12    }13    return 0;14}
输出
不及格:2 人
第一个超过 90 的:91

和第 12 课的 std::count、std::find 比:那两个只能数、找「等于某个值」的;带 _if 的,条件随你写。(迭代器第 27 课再细讲,现在知道 *it 取值就够了。)

for_each

std::for_each 对每个元素调用一次你给的函数。参数写成引用,就能改元素:

main.cpp
1#include <algorithm>2#include <iostream>3#include <vector>4 5int main() {6    std::vector<double> prices{20, 35, 12.5};7    std::for_each(prices.begin(), prices.end(), [](double& p) { p = p * 0.8; });8    std::for_each(prices.begin(), prices.end(), [](double p) {9        std::cout << p << " 元\n";10    });11    return 0;12}
输出
16 元
28 元
10 元

说实话,这样的事用范围 for 一样清楚。for_each 的好处是:手里已经有一个存在变量里的 lambda 时,直接交给它就行。

捕获:用外面的变量

lambda 里想用外面的变量,比如一个及格线,直接写会编译错误:

main.cpp
1#include <algorithm>2#include <iostream>3#include <vector>4 5int main() {6    std::vector<int> scores{72, 58, 91, 45, 66};7    int pass_line = 70;8    auto passed = std::count_if(scores.begin(), scores.end(), [](int s) { return s >= pass_line; });9    std::cout << passed << '\n';10    return 0;11}
输出
main.cpp: In lambda function:
main.cpp:8:87: error: 'pass_line' is not captured
    8 |     auto passed = std::count_if(scores.begin(), scores.end(), [](int s) { return s >= pass_line; });
      |                                                                                       ^~~~~~~~~

'pass_line' is not captured:pass_line 没有被捕获。lambda 默认看不到外面的局部变量,要在方括号里列出来,叫捕获。写成 [pass_line] 就好了,结果是 2。

[pass_line] 是按值捕获:建 lambda 的那一刻,把 pass_line 的值复制一份带进去。之后外面再改,lambda 里的不变:

先猜一猜

这段程序显示什么?

1#include <iostream>2 3int main() {4    int bonus = 5;5    auto add_bonus = [bonus](int s) { return s + bonus; };6    bonus = 100;7    std::cout << add_bonus(80) << '\n';8    return 0;9}

按引用捕获

要让 lambda 改外面的变量,或者看到它最新的值,就按引用捕获:[&total]。lambda 里的 total 是外面那个的引用:

main.cpp
1#include <algorithm>2#include <iostream>3#include <vector>4 5int main() {6    std::vector<int> steps{4200, 8100, 6500, 9900};7    int total = 0;8    int good_days = 0;9    int goal = 6000;10    std::for_each(steps.begin(), steps.end(), [&total, &good_days, goal](int s) {11        total += s;12        if (s >= goal) {13            good_days++;14        }15    });16    std::cout << "一共 " << total << " 步," << good_days << " 天达标\n";17    return 0;18}
输出
一共 28700 步,3 天达标

点单步:lambda 每运行一次,main 里的 total 和 good_days 就变一次。goal 只看不改,按值捕获就行。

写法意思
[]什么也不捕获
[x]复制 x
[&x]引用 x
[=]用到的外面变量都复制
[&]用到的外面变量都引用

注意: 按值捕获的变量在 lambda 里是只读的,[total](int s) { total += s; } 是编译错误。按引用捕获时要小心:lambda 用的时候,被引用的变量必须还活着。

std::function

lambda 的类型写不出来,存进变量靠 auto。可要是想让一个变量先存这个 lambda、以后换成那个,或者写一个「参数是一个函数」的函数,就用 <functional> 里的 std::function。尖括号里写「返回类型(参数类型)」:

main.cpp
1#include <functional>2#include <iostream>3#include <vector>4 5void show_prices(const std::vector<double>& prices, std::function<double(double)> rule) {6    for (double p : prices) {7        std::cout << rule(p) << " ";8    }9    std::cout << '\n';10}11 12int main() {13    std::vector<double> prices{20, 50, 120};14    std::function<double(double)> rule = [](double p) { return p; };15    show_prices(prices, rule);16    rule = [](double p) { return p * 0.9; };17    show_prices(prices, rule);18    double cut = 15;19    show_prices(prices, [cut](double p) { return p >= 100 ? p - cut : p; });20    return 0;21}
输出
20 50 120 
18 45 108 
20 50 105 

std::function<double(double)> 能装下任何「给一个 double、还一个 double」的东西:不同的 lambda,带捕获的也行,普通函数也行。show_prices 不管规矩是什么,只管调用它。(最后一个 lambda 里的 条件 ? 甲 : 乙:条件成立取甲,不然取乙。)

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

便宜的排前面

用 std::sort 和一个 lambda,按价格从低到高排好商品,再按顺序显示名字。程序要显示三行:鸡蛋、牛奶、咖啡豆。

要显示
鸡蛋
牛奶
咖啡豆
exercise1.cpp
1#include <algorithm>2#include <iostream>3#include <string>4#include <vector>5 6struct Product {7    std::string name;8    double price;9};10 11int main() {12    std::vector<Product> cart{{"咖啡豆", 68}, {"鸡蛋", 15.5}, {"牛奶", 22}};13    // 在这里排序14    for (const auto& p : cart) {15        std::cout << p.name << '\n';16    }17    return 0;18}
输出
点「运行」或按 Ctrl+Enter
练习 2

热天有几天

用 std::count_if 数一数 temps 里有几天高于 hot(捕获它)。程序要显示 高温:3 天。

要显示
高温:3 天
exercise2.cpp
1#include <algorithm>2#include <iostream>3#include <vector>4 5int main() {6    std::vector<int> temps{29, 33, 35, 31, 36, 28};7    int hot = 32;8    // 在这里数9    return 0;10}
输出
点「运行」或按 Ctrl+Enter
练习 3

修好它:改不了的总数

这段代码想用 for_each 把账单加起来,却编译不过。只加一个字符,让程序显示 合计:86。

要显示
合计:86
exercise3.cpp
1#include <algorithm>2#include <iostream>3#include <vector>4 5int main() {6    std::vector<int> bills{30, 12, 44};7    int total = 0;8    std::for_each(bills.begin(), bills.end(), [total](int b) { total += b; });9    std::cout << "合计:" << total << '\n';10    return 0;11}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • lambda 是就地写的小函数:[捕获](参数) { 函数体 },返回类型自己推;存进变量用 auto。
  • 给 std::sort 一个 lambda 定排序规矩,比如 [](int a, int b) { return a > b; }。
  • std::count_if、std::find_if 要一个回答 true / false 的 lambda;std::for_each 对每个元素调用一次。
  • lambda 要用外面的局部变量,得捕获:[x] 复制一份(建的时候的值,只读),[&x] 是引用(能改,看到最新的值)。
  • [=] 全部复制,[&] 全部引用。
  • std::function<返回类型(参数类型)> 能装下各种函数和 lambda,用来存起来、换掉,或者当参数传。
← 上一课智能指针下一课 · 即将开放继承