Lambda捕获
本节阅读量:
捕获子句和按值捕获
在上一课,我们介绍了以下示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
int main()
{
std::array<std::string_view, 4> arr{ "apple", "banana", "walnut", "lemon" };
auto found{ std::find_if(arr.begin(), arr.end(),
[](std::string_view str)
{
return str.find("nut") != std::string_view::npos;
}) };
if (found == arr.end())
{
std::cout << "No nuts\n";
}
else
{
std::cout << "Found " << *found << '\n';
}
return 0;
}
|
现在,让我们修改nut示例,并让用户选择要搜索的子字符串。这并不像您预期的那样直观。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
#include <string>
int main()
{
std::array<std::string_view, 4> arr{ "apple", "banana", "walnut", "lemon" };
// 询问用户需要搜索的字符串.
std::cout << "search for: ";
std::string search{};
std::cin >> search;
auto found{ std::find_if(arr.begin(), arr.end(), [](std::string_view str) {
// 搜索 @search 而不是 "nut".
return str.find(search) != std::string_view::npos; // 错误: search 无法在此作用域内使用
}) };
if (found == arr.end())
{
std::cout << "Not found\n";
}
else
{
std::cout << "Found " << *found << '\n';
}
return 0;
}
|
此代码无法编译。与普通嵌套块不同,普通嵌套块可以访问外部块中的任何标识符,而lambda只能访问在lambda外部定义的某些类型的对象。这包括:
- 具有静态(或线程局部)存储期的对象(这包括全局变量和静态局部变量)
- constexpr对象(显式或隐式)
由于search不满足这些要求,所以lambda无法看到它。
要从lambda中访问search,我们需要使用捕获子句。
捕获子句
捕获子句用于(间接)让lambda访问外层作用域中的变量,而lambda通常无权访问这些变量。我们需要做的,只是将要从lambda中访问的实体列为捕获子句的一部分。在这种情况下,我们希望让lambda访问变量search的值,因此将其添加到捕获子句中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
#include <string>
int main()
{
std::array<std::string_view, 4> arr{ "apple", "banana", "walnut", "lemon" };
std::cout << "search for: ";
std::string search{};
std::cin >> search;
// 捕获 @search vvvvvv
auto found{ std::find_if(arr.begin(), arr.end(), [search](std::string_view str) {
return str.find(search) != std::string_view::npos;
}) };
if (found == arr.end())
{
std::cout << "Not found\n";
}
else
{
std::cout << "Found " << *found << '\n';
}
return 0;
}
|
用户现在可以搜索数组的元素。
输出
1
2
|
search for: nana
Found banana
|
那么捕获实际上是如何工作的呢?
虽然上面示例中的lambda看起来像是直接访问main中的search变量,但情况并非如此。Lambda可能看起来像嵌套块,但它们的工作方式略有不同。
当执行lambda定义时,对于lambda捕获的每个变量,lambda中都会创建该变量的克隆变量(具有相同的名称)。此时,这些克隆变量会用外部作用域中同名变量的值进行初始化。
因此,在上面的示例中,当创建lambda对象时,lambda会获得自己的克隆变量search。这个克隆的search与main中的search具有相同的值,因此它的行为看起来就像是在访问main中的search。
虽然这些克隆的变量具有相同的名称,但它们不一定具有与原始变量相同的类型。我们将在本课接下来的部分中探索这一点。
关键点
lambda的捕获变量是外部作用域变量的副本,而不是实际变量。
对于高级读者
尽管lambda看起来像函数,但它们实际上是可以像函数一样调用的对象(这些对象称为functor——我们将在以后的课程中讨论如何从头开始创建自己的functor)。
当编译器遇到lambda定义时,它为lambda创建自定义对象定义。每个捕获的变量都成为对象的数据成员。
在运行时,当遇到lambda定义时,会实例化lambda对象,并在此时初始化lambda的成员。
默认情况下,捕获被视为常量
调用lambda时,会调用lambda对象的operator()。默认情况下,此运算符()会将捕获视为常量,这意味着lambda不允许修改这些捕获。
在下面的例子中,我们捕获变量ammo并尝试减少它。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#include <iostream>
int main()
{
int ammo{ 10 };
// 定义 lambda 捕获 ammo.
auto shoot{
[ammo]() {
// 非法, ammo 不能被修改.
--ammo;
std::cout << "Pew! " << ammo << " shot(s) left.\n";
}
};
// 调用lambda
shoot();
std::cout << ammo << " shot(s) left\n";
return 0;
}
|
上面的代码不会编译,因为ammo在lambda中被视为const。
可修改的捕获
为了允许修改捕获的变量,我们可以将lambda标记为mutable:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
#include <iostream>
int main()
{
int ammo{ 10 };
auto shoot{
[ammo]() mutable { // 现在是 mutable
// 允许修改 ammo
--ammo;
std::cout << "Pew! " << ammo << " shot(s) left.\n";
}
};
shoot();
shoot();
std::cout << ammo << " shot(s) left\n";
return 0;
}
|
输出:
1
2
3
|
Pew! 9 shot(s) left.
Pew! 8 shot(s) left.
10 shot(s) left
|
虽然现在可以编译,但仍然存在逻辑错误。发生了什么?当shoot被创建时,它捕获了一个ammo的副本。当lambda将ammo从10减少到9再到8时,它减少的是自己的副本,而不是main()中的原始ammo值。
请注意,在多次调用lambda之间,它内部ammo的值会被保留下来!
警告
因为捕获的变量是lambda对象的成员,所以它们的值在对lambda的多个调用中被持久化!
通过引用捕获
就像函数可以更改通过引用传递的参数值一样,我们也可以通过引用捕获变量,以允许lambda影响外部变量的值。
为了通过引用捕获变量,我们在捕获列表中的变量名前面加上一个与号(&)。与按值捕获的变量不同,通过引用捕获的变量是非常量,除非它们捕获的变量本身为常量。每当您希望像通过引用传递函数参数那样处理变量时(例如,对于非基本类型),都应优先考虑通过引用捕获,而不是通过值捕获。
下面是上面的代码,其中包含通过引用捕获的ammo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
#include <iostream>
int main()
{
int ammo{ 10 };
auto shoot{
// 不需要标记为 mutable
[&ammo]() { // &ammo 表示 ammo 按引用捕获
// 修改 ammo 会影响 main 中的 ammo
--ammo;
std::cout << "Pew! " << ammo << " shot(s) left.\n";
}
};
shoot();
std::cout << ammo << " shot(s) left\n";
return 0;
}
|
这会产生预期的答案:
1
2
|
Pew! 9 shot(s) left.
9 shot(s) left
|
现在,让我们使用引用捕获来计算std::sort在对数组进行排序时进行了多少比较。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
struct Car
{
std::string_view make{};
std::string_view model{};
};
int main()
{
std::array<Car, 3> cars{ { { "Volkswagen", "Golf" },
{ "Toyota", "Corolla" },
{ "Honda", "Civic" } } };
int comparisons{ 0 };
std::sort(cars.begin(), cars.end(),
// 按引用捕获 @comparisons
[&comparisons](const auto& a, const auto& b) {
// 可以修改main函数中的comparisons
++comparisons;
// Sort the cars by their make.
return a.make < b.make;
});
std::cout << "Comparisons: " << comparisons << '\n';
for (const auto& car : cars)
{
std::cout << car.make << ' ' << car.model << '\n';
}
return 0;
}
|
可能的输出:
1
2
3
4
|
Comparisons: 2
Honda Civic
Toyota Corolla
Volkswagen Golf
|
捕获多个变量
可以通过用逗号分隔多个变量来捕获它们。这可以包括通过值或引用捕获的变量的组合:
1
2
3
4
5
6
|
int health{ 33 };
int armor{ 100 };
std::vector<CEnemy> enemies{};
// 按值捕获 health 和 armor, 按引用捕获 enemies.
[health, armor, &enemies](){};
|
默认捕获
必须显式列出要捕获的变量可能会很麻烦。如果修改lambda,可能会忘记添加或删除捕获的变量。幸运的是,我们可以借助编译器自动生成需要捕获的变量列表。
默认捕获会自动捕获lambda中用到的所有变量。
要按值捕获所有使用的变量,请使用“=”。要通过引用捕获所有使用的变量,请使用“&”。
下面是使用默认按值捕获的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
#include <algorithm>
#include <array>
#include <iostream>
int main()
{
std::array areas{ 100, 25, 121, 40, 56 };
int width{};
int height{};
std::cout << "Enter width and height: ";
std::cin >> width >> height;
auto found{ std::find_if(areas.begin(), areas.end(),
[=](int knownArea) { // 按值捕获 width 和 height
return width * height == knownArea; // 因为它们在这里被使用了
}) };
if (found == areas.end())
{
std::cout << "I don't know this area :(\n";
}
else
{
std::cout << "Area found :)\n";
}
return 0;
}
|
默认捕获可以与正常捕获混合。我们可以通过值捕获一些变量,通过引用捕获其他变量,但每个变量只能捕获一次。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
int health{ 33 };
int armor{ 100 };
std::vector<CEnemy> enemies{};
// health 与 armor 按值捕获, enemies 按引用捕获.
[health, armor, &enemies](){};
// enemies 按引用捕获,其余全按值捕获
[=, &enemies](){};
// armor 按值捕获,其余全按引用捕获
[&, armor](){};
// 非法, armor被按引用捕获了两次
[&, &armor](){};
// 非法, armor被按值捕获了两次
[=, armor](){};
// 非法, armor 出现了两次
[armor, &health, &armor](){};
// 非法, 默认捕获需要是捕获列表中的第一项
[armor, &](){};
|
在lambda捕获中定义新变量
有时,我们希望捕获一个稍微修改过的变量,或者声明一个仅在lambda范围内可见的新变量。我们可以通过在lambda捕获中定义变量而不指定其类型来实现这一点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
#include <array>
#include <iostream>
#include <algorithm>
int main()
{
std::array areas{ 100, 25, 121, 40, 56 };
int width{};
int height{};
std::cout << "Enter width and height: ";
std::cin >> width >> height;
// 使用userArea,从捕获的width和height计算
auto found{ std::find_if(areas.begin(), areas.end(),
// 新声明的变量userArea只在lambda内可见
// userArea 的类型被自动推导为 int.
[userArea{ width * height }](int knownArea) {
return userArea == knownArea;
}) };
if (found == areas.end())
{
std::cout << "I don't know this area :(\n";
}
else
{
std::cout << "Area found :)\n";
}
return 0;
}
|
定义lambda时,userArea只会计算一次。计算得到的面积存储在lambda对象中,并且每次调用时都是相同的。如果lambda是可变的,并且修改了捕获中定义的变量,则原始值会被覆盖。
最佳实践
仅当变量的值较短且类型明显时,才在捕获中初始化变量。否则,最好在lambda之外定义变量并捕获它。
悬空捕获的变量
变量会在定义lambda的位置被捕获。如果按引用捕获的变量在lambda之前死亡,则lambda将保留一个悬空引用。
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#include <iostream>
#include <string>
// 返回一个lambda
auto makeWalrus(const std::string& name)
{
// 按引用捕获name并返回lambda
return [&]() {
std::cout << "I am a walrus, my name is " << name << '\n'; // 未定义的行为
};
}
int main()
{
// 创建一个新的 walrus,name 是 Roofus.
// sayName 是 makeWalrus 返回的lambda
auto sayName{ makeWalrus("Roofus") };
// 调用 makeWalrus 返回的lambda.
sayName();
return 0;
}
|
对makeWalrus的调用会从字符串字面值“Roofus”创建临时std::string。makeWalrus中的lambda通过引用捕获这个临时字符串。当makeWalrus返回时,临时字符串死亡,但lambda仍然引用它。然后,当我们调用sayName时,就会访问悬空引用,导致未定义的行为。
请注意,如果name通过值传递给makeWalrus,也会发生这种情况。变量name仍然会在makeWalrus的末尾死亡,而lambda保留了一个悬空引用。
如果希望在使用lambda时捕获的名称有效,则需要改为按值捕获它(显式或使用默认的按值捕获)。
警告
通过引用捕获变量时要格外小心,特别是使用默认按引用捕获时。捕获的变量必须比lambda寿命更长。
可变lambda的意外副本
因为lambda是对象,所以可以复制它们。在某些情况下,这可能会导致问题。请看以下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
#include <iostream>
int main()
{
int i{ 0 };
// 创建一个名为 count 的新 lambda
auto count{ [i]() mutable {
std::cout << ++i << '\n';
} };
count(); // 调用 count
auto otherCount{ count }; // 创建 count 的副本
// 调用 count 和 副本
count();
otherCount();
return 0;
}
|
输出
代码并没有打印1、2、3,而是打印了两次2。当我们创建otherCount作为count的副本时,我们创建的是count当前状态的副本。count的i是1,所以otherCount的i也是1。由于otherCount是count的副本,因此它们各自都有自己的i。
现在,让我们看一个稍微不太明显的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#include <iostream>
#include <functional>
void myInvoke(const std::function<void()>& fn)
{
fn();
}
int main()
{
int i{ 0 };
// 递增并打印存储的 @i.
auto count{ [i]() mutable {
std::cout << ++i << '\n';
} };
myInvoke(count);
myInvoke(count);
myInvoke(count);
return 0;
}
|
输出:
这以更模糊的形式展示了与前面示例相同的问题。
当我们调用myInvoke(count)时,编译器会看到count(具有lambda类型)与引用参数类型(std::function<void()>)不匹配。它会把lambda转换为临时std::function,以便引用参数可以绑定到它,而这会制作lambda的副本。因此,对fn()的调用实际上是在lambda的副本上执行的,该副本作为临时std::function的一部分存在,而不是实际的lambda。
如果我们需要传递可变的lambda,并希望避免无意中复制的可能性,则有两种选择。一种选择是改用非捕获lambda——在上面的例子中,我们可以删除捕获并改用静态局部变量来跟踪状态。但静态局部变量可能很难跟踪,并使代码的可读性降低。一个更好的选择是首先防止复制我们的lambda。但由于我们不能影响std::function(或其他标准库函数或对象)的实现方式,我们如何才能做到这一点?
一个选项是立即将lambda放入std::function中。这样,当我们调用myInvoke()时,引用参数fn可以绑定到我们的std::function,并且不会生成临时副本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#include <iostream>
#include <functional>
void myInvoke(const std::function<void()>& fn)
{
fn();
}
int main()
{
int i{ 0 };
// 递增并打印存储的 @i.
std::function count{ [i]() mutable { // lambda 存储在 std::function
std::cout << ++i << '\n';
} };
myInvoke(count); // 调用时不会创建副本
myInvoke(count); // 调用时不会创建副本
myInvoke(count); // 调用时不会创建副本
return 0;
}
|
现在输出符合预期:
另一种解决方案是使用引用包装器。C++提供了一种方便的类型(作为<functional>头文件的一部分),称为std::reference_wrapper,它允许我们像传递引用一样传递普通类型。为了更方便,可以使用std::ref()函数创建std::reference_wrapper。通过将lambda包装在std::reference_wrapper中,每当有人试图复制我们的lambda时,他们都会复制reference_wrapper(从而避免复制lambda)。
下面是我们使用std::ref更新后的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
#include <iostream>
#include <functional> // 引入 std::reference_wrapper 和 std::ref
void myInvoke(const std::function<void()>& fn)
{
fn();
}
int main()
{
int i{ 0 };
// 递增并打印存储的 @i.
auto count{ [i]() mutable {
std::cout << ++i << '\n';
} };
// std::ref(count) 确保count行为像引用一样
// 任何拷贝count的行为,都像是拷贝了count的引用
// 这样只会有一个count存在
myInvoke(std::ref(count));
myInvoke(std::ref(count));
myInvoke(std::ref(count));
return 0;
}
|
现在输出符合预期:
该方法的有趣之处在于,即使myInvoke按值(而不是按引用)获取fn,它也可以工作!
规则
标准库函数可能会复制函数对象(提示:lambda是函数对象)。如果要为lambda提供可变的捕获变量,请使用std::ref通过引用传递它们。
最佳实践
尽量避免可变lambda。不可变lambda更容易理解,并且不会受到上述问题的影响,也不会遇到可变lambda在添加并行执行时可能出现的更危险问题。
习题
问题 1
下面哪些变量可以在main中的lambda里不经显式捕获直接使用?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
int i{};
static int j{};
int getValue()
{
return 0;
}
int main()
{
int a{};
constexpr int b{};
static int c{};
static constexpr int d{};
const int e{};
const int f{ getValue() };
static const int g{};
static const int h{ getValue() };
[](){
// 试着在不显式捕获的情况下使用这些变量。
a;
b;
c;
d;
e;
f;
g;
h;
i;
j;
}();
return 0;
}
|
查看答案
| 变量 |
能否不经显式捕获使用 |
a |
不能。a具有自动存储期。 |
b |
能。b可用于常量表达式。 |
c |
能。c具有静态存储期。 |
d |
能。 |
e |
能。e可用于常量表达式。 |
f |
不能。f的值取决于getValue,这可能需要程序运行后才能确定。 |
g |
能。 |
h |
能。h具有静态存储期。 |
i |
能。i是全局变量。 |
j |
能。j在整个文件中都可访问。 |
问题 2
下面这段代码会打印什么?不要运行它,在脑中推算。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
#include <iostream>
#include <string>
int main()
{
std::string favoriteFruit{ "grapes" };
auto printFavoriteFruit{
[=]() {
std::cout << "I like " << favoriteFruit << '\n';
}
};
favoriteFruit = "bananas with chocolate";
printFavoriteFruit();
return 0;
}
|
查看答案
输出为:
I like grapes
printFavoriteFruit按值捕获了favoriteFruit。修改main中的favoriteFruit不会影响lambda中的favoriteFruit。
问题 3
我们来写一个关于平方数的小游戏(平方数是指由某个整数与自身相乘得到的数:1、4、9、16、25……)。
游戏准备:
- 请用户输入一个起始数字(例如3)。
- 询问用户要生成多少个值。
- 在2到4之间随机选取一个整数,作为乘数。
- 生成用户指定个数的值。从起始数字开始,每个值应当是下一个平方数乘以该乘数。
游戏玩法:
- 用户输入一个猜测值。
- 如果该猜测与某个已生成的值匹配,就把该值从列表中移除,用户继续猜。
- 如果用户猜出了所有生成的值,就获胜。
- 如果猜测与任何已生成的值都不匹配,用户失败,程序告诉他最接近的未猜中值。
下面是几次示例过程,帮助你更好地理解游戏玩法:
Start where? 4
How many? 5
I generated 5 square numbers. Do you know what each number is after multiplying it by 2?
> 32
Nice! 4 number(s) left.
> 72
Nice! 3 number(s) left.
> 50
Nice! 2 number(s) left.
> 126
126 is wrong! Try 128 next time.
- 从4开始,程序生成接下来的5个平方数:16、25、36、49、64
- 程序随机选了2作为乘数,于是每个平方数都乘以2:32、50、72、98、128
- 现在轮到用户猜了。
- 32在列表中。
- 72在列表中。
- 126不在列表中,因此用户失败。最接近的未猜中数字是128。
Start where? 1
How many? 3
I generated 3 square numbers. Do you know what each number is after multiplying it by 4?
> 4
Nice! 2 number(s) left.
> 16
Nice! 1 number(s) left.
> 36
Nice! You found all numbers, good job!
- 从1开始,程序生成接下来的3个平方数:1、4、9
- 程序随机选了4作为乘数,于是每个平方数都乘以4:4、16、36
- 用户把所有数字都猜对了,赢得游戏。
提示:
- 使用 Random.h(「8.14 全局随机数」)生成随机数。
- 使用
std::find()(「18.2 算法标准库简介」)在列表中查找数字。
- 使用
std::vector::erase()移除元素,例如
1
2
3
4
5
|
auto found{ std::find(/* ... */) };
// 确认元素已找到
myVector.erase(found);
|
- 使用
std::min_element和一个lambda找出最接近用户猜测的数字。std::min_element的用法与上一题中的std::max_element类似。
显示提示
使用中的std::abs计算两个数字之间的正差值。
1
|
int distance{ std::abs(3 - 5) }; // 2
|
查看答案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
|
#include <algorithm> // std::find, std::min_element
#include <cmath> // std::abs
#include <cstddef> // std::size_t
#include <iostream>
#include <vector>
#include "Random.h"
using Numbers = std::vector<int>;
namespace config
{
constexpr int multiplierMin{ 2 };
constexpr int multiplierMax{ 4 };
}
// 从 @start*@start 开始生成 @count 个数字,并把
// 每个平方数都乘以 @multiplier。
Numbers generateNumbers(int start, int count, int multiplier)
{
Numbers numbers(static_cast<std::size_t>(count));
for (int index = 0; index < count; ++index)
{
std::size_t uindex{ static_cast<std::size_t>(index) };
numbers[uindex] = (start + index) * (start + index) * multiplier;
}
return numbers;
}
// 询问用户起始数字,然后生成一组数字
Numbers setupGame()
{
int start{};
std::cout << "Start where? ";
std::cin >> start;
int count{};
std::cout << "How many? ";
std::cin >> count;
int multiplier{ Random::get(config::multiplierMin, config::multiplierMax) };
std::cout << "I generated " << count
<< " square numbers. Do you know what each number is after multiplying it by "
<< multiplier << "?\n";
return generateNumbers(start, count, multiplier);
}
// 返回用户的猜测
int getUserGuess()
{
int guess{};
std::cout << "> ";
std::cin >> guess;
return guess;
}
// 在 @numbers 中查找 @guess 并把它删除。
// 找到该值返回 true,否则返回 false。
bool findAndRemove(Numbers& numbers, int guess)
{
auto found{ std::find(numbers.begin(), numbers.end(), guess) };
if (found == numbers.end())
return false;
numbers.erase(found);
return true;
}
// 找出 @numbers 中最接近 @guess 的值。
int findClosestNumber(const Numbers& numbers, int guess)
{
return *std::min_element(numbers.begin(), numbers.end(),
[=](int a, int b)
{
return std::abs(a - guess) < std::abs(b - guess);
});
}
// 用户猜对数字时调用。
void printSuccess(const Numbers& numbers)
{
std::cout << "Nice! ";
if (numbers.size() == 0)
{
std::cout << "You found all numbers, good job!\n";
}
else
{
std::cout << numbers.size() << " number(s) left.\n";
}
}
// 用户猜的数字不在列表中时调用。
void printFailure(const Numbers& numbers, int guess)
{
int closest{ findClosestNumber(numbers, guess) };
std::cout << guess << " is wrong!\n";
std::cout << "Try " << closest << " next time.\n";
}
int main()
{
Numbers numbers{ setupGame() };
while (true)
{
int guess{ getUserGuess() };
if (!findAndRemove(numbers, guess))
{
printFailure(numbers, guess);
break;
}
printSuccess(numbers);
if (numbers.size() == 0)
break;
}
return 0;
}
|