11.14 – Лямбда-захваты
Списки захвата и захват по значению
В предыдущем уроке (11.13 – Введение в лямбды (анонимные функции)) мы представили следующий пример:
#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;
}
Теперь давайте изменим его и позволим пользователю выбрать подстроку для поиска. Это не так интуитивно понятно, как можно было ожидать.
#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;
}
Этот код не компилируется. В отличие от вложенных блоков, где любой идентификатор, определенный во внешнем блоке, доступен в области видимости вложенного блока, лямбда-выражения могут обращаться только к определенным видам идентификаторов: глобальным идентификаторам, сущностям, известным во время компиляции, и сущностям со статической продолжительностью хранения. Переменная search
не выполняет ни одно из этих требований, поэтому лямбда ее не видит. Вот для чего нужен список захвата.
Список захвата
Список захвата используется, чтобы (косвенно) предоставить лямбде доступ к переменным, доступным в окружающей области видимости, к которым она в обычном режиме не имеет доступа. Всё, что нам нужно сделать, это перечислить объекты, к которым мы хотим получить доступ из лямбда-выражения, как часть списка захвата. В этом случае мы хотим предоставить нашей лямбда-функции доступ к значению переменной search
, поэтому мы добавляем ее в списке захвата:
#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 ↓↓↓↓↓↓
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;
}
Теперь пользователь может искать элемент нашего массива.
Вывод программы:
search for: nana
Found banana
Так как же на самом деле работают захваты?
Хотя может показаться, что наша лямбда в приведенном выше примере напрямую обращается к значению переменной search
из main
, это не так. Лямбды могут выглядеть как вложенные блоки, но работают они немного по-другому (и это различие важно).
Когда выполняется определение лямбда-выражения, для каждой переменной, которую захватывает лямбда, внутри лямбды создается клон этой переменной (с таким же именем). Эти клонированные переменные инициализируются на этом этапе из одноименных переменных внешней области видимости.
Таким образом, в приведенном выше примере, когда создается объект лямбда, лямбда получает свою собственную клонированную переменную с именем search
. Этот клон search
имеет то же значение, что и search
в main
, поэтому он ведет себя так, как будто мы получаем доступ к search
в main
, но это не так.
Хотя эта клонированная переменная имеет то же имя, она не обязательно имеет тот же тип, что и исходная переменная. Мы рассмотрим это в следующих разделах этого урока.
Ключевые выводы
Захваченные переменные лямбда-выражения являются клонами переменных внешней области видимости, а не фактическими переменными.
Для продвинутых читателей
Хотя лямбды выглядят как функции, на самом деле они являются объектами, которые можно вызывать как функции (они называются функторами – мы обсудим, как создавать свои собственные функторы с нуля в будущих уроках).
Когда компилятор встречает определение лямбда-выражения, он создает для лямбды определение настраиваемого объекта. Каждая захваченная переменная становится членом данных этого объекта.
Во время выполнения, когда встречается определение лямбды, создается экземпляр объекта лямбды, и в этой точке инициализируются члены лямбды.
По умолчанию захват выполняется в константное значение
По умолчанию переменные захватываются константными значениями. Это означает, что когда лямбда создается, лямбда захватывает константную копию переменной внешней области видимости, что означает, что лямбда не может ее изменять. В следующем примере мы захватываем переменную ammo
и пытаемся уменьшить ее значение.
#include <iostream>
int main()
{
int ammo{ 10 };
// Определить лямбду и сохранить ее в переменной с именем "shoot".
auto shoot{
[ammo]() {
// Недопустимо, ammo была захвачена как константная копия.
--ammo;
std::cout << "Pew! " << ammo << " shot(s) left.\n";
}
};
// Вызов лямбды
shoot();
std::cout << ammo << " shot(s) left\n";
return 0;
}
В приведенном выше примере, когда мы захватываем переменную ammo
, в лямбде создается новая константная переменная с тем же именем и значением. Мы не можем изменить ее, потому что это константа, что вызывает ошибку компиляции.
Неконстантный захват по значению
Чтобы разрешить модификацию переменных, которые были захвачены по значению, мы можем пометить лямбда как mutable
(изменяемую). Ключевое слово mutable
в этом контексте удаляет квалификацию const
из всех переменных, захваченных по значению.
#include <iostream>
int main()
{
int ammo{ 10 };
auto shoot{
// Добавлен спецификатор mutable после списка параметров.
[ammo]() mutable {
// Теперь нам разрешено изменять ammo
--ammo;
std::cout << "Pew! " << ammo << " shot(s) left.\n";
}
};
shoot();
shoot();
std::cout << ammo << " shot(s) left\n";
return 0;
}
Вывод программы:
Pew! 9 shot(s) left.
Pew! 8 shot(s) left.
10 shot(s) left
Хотя код компилируется, логическая ошибка всё еще остается. Что случилось? Когда лямбда была вызвана, она захватила копию ammo
. Когда лямбда уменьшала значение ammo
с 10 до 9 и до 8, она уменьшала свою собственную копию, а не исходное значение.
Обратите внимание, что значение ammo
сохраняется при вызовах лямбды!
Захват по ссылке
Подобно тому, как функции могут изменять значение аргументов, передаваемых по ссылке, мы также можем захватывать переменные по ссылке, чтобы позволить нашей лямбде влиять на значение аргумента.
Чтобы захватить переменную по ссылке, мы добавляем амперсанд (&
) к имени переменной в списке захвата. В отличие от переменных, которые захватываются по значению, переменные, которые захватываются по ссылке, не являются константными, если только переменная, которую они захватывают, сама не является константой. Захват по ссылке должен быть предпочтительнее захвата по значению в тех же случаях, когда бывает предпочтительной передача аргумента в функцию по ссылке (например, для небазовых типов).
Вот тот же код, что и выше, с переменной ammo
, полученной по ссылке:
#include <iostream>
int main()
{
int ammo{ 10 };
auto shoot{
// Нам больше не нужен спецификатор mutable
[&ammo]() { // &ammo означает, что ammo захвачена по ссылке
// Изменения в ammo повлияют на ammo в main
--ammo;
std::cout << "Pew! " << ammo << " shot(s) left.\n";
}
};
shoot();
std::cout << ammo << " shot(s) left\n";
return 0;
}
Это дает ожидаемый ответ:
Pew! 9 shot(s) left.
9 shot(s) left
Теперь давайте воспользуемся захватом по ссылке, чтобы подсчитать, сколько сравнений делает std::sort
при сортировке массива.
#include <algorithm>
#include <array>
#include <iostream>
#include <string>
struct Car
{
std::string make{};
std::string 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) {
// Мы захватили comparisons по ссылке и
// поэтому можем изменить его без "mutable".
++comparisons;
// Сортировка автомобилей по марке.
return (a.make < b.make);
});
std::cout << "Comparisons: " << comparisons << '\n';
for (const auto& car : cars)
{
std::cout << car.make << ' ' << car.model << '\n';
}
return 0;
}
Возможный вывод программы:
Comparisons: 2
Honda Civic
Toyota Corolla
Volkswagen Golf
Захват нескольких переменных
Несколько переменных можно захватить, разделив их запятыми в списке захвата. Это может быть сочетание переменных, захватываемых по значению или по ссылке:
int health{ 33 };
int armor{ 100 };
std::vector<CEnemy> enemies{};
// Захватываем health и armor по значению, а enemies по ссылке
[health, armor, &enemies](){};
Захват по умолчанию
Необходимость явно указывать переменные, которые вы хотите захватить, может быть утомительной. Если вы измените лямбда-выражение, вы можете забыть добавить или удалить захватываемые переменные. К счастью, мы можем заручиться помощью компилятора для автоматического создания списка переменных, которые нам нужно захватить.
Захват по умолчанию захватывает все переменные, упомянутые в лямбда-выражении. Переменные, не упомянутые в лямбда-выражении, если используется захват по умолчанию, не захватываются.
- Чтобы захватить все используемые переменные по значению, используйте значение захвата
=
. - Чтобы захватить все используемые переменные по ссылке, используйте значение захвата
&
.
Вот пример использования захвата по умолчанию по значению:
#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(),
// по умолчанию захватывает width и height по значению
[=](int knownArea) {
// потому что они здесь упоминаются
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;
}
Захваты по умолчанию можно смешивать с обычными захватами. Мы можем захватывать одни переменные по значению, а другие по ссылке, но каждая переменная может быть захвачена только один раз.
int health{ 33 };
int armor{ 100 };
std::vector<CEnemy> enemies{};
// Захватываем health и armor по значению, а enemies по ссылке.
[health, armor, &enemies](){};
// Захватываем enemies по ссылке, а всё остальное по значению.
[=, &enemies](){};
// Захватываем armor по значению, а всё остальное по ссылке.
[&, armor](){};
// Недопустимо, мы уже сказали, что хотим захватить всё по ссылке.
[&, &armor](){};
// Недопустимо, мы уже сказали, что хотим захватить всё по значению.
[=, armor](){};
// Недопустимо, armor появляется дважды.
[armor, &health, &armor](){};
// Недопустимо, захват по умолчанию должен быть первым элементом в списке захвата.
[armor, &](){};
Определение новых переменных в лямбда-захвате
Иногда мы хотим захватить переменную с небольшими изменениями или объявить новую переменную, которая видна только в области видимости лямбды. Мы можем сделать это, определив переменную в лямбда-захвате без указания ее типа.
#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(),
// Объявляем новую переменную, которая видна только лямбде.
// Тип 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;
}
userArea
будет вычисляться только один раз, при определении лямбды. Рассчитанная площадь сохраняется в лямбда-объекте и одинакова для каждого вызова. Если лямбда изменяема и изменяет переменную, которая была определена в захвате, исходное значение будет переопределено.
Лучшая практика
Инициализируйте переменные в списке захвата только в том случае, если их значение короткое и их тип очевиден. В противном случае лучше всего определить переменную вне лямбды и захватить ее.
Висячие захваченные переменные
Переменные захватываются в точке определения лямбды. Если переменная, захваченная по ссылке, умирает раньше лямбды, лямбда останется с висячей ссылкой.
Например:
#include <iostream>
#include <string>
// возвращает лямбду
auto makeWalrus(const std::string& name)
{
// Захватить name по ссылке и вернуть лямбду.
return [&]() {
std::cout << "I am a walrus, my name is " << name << '\n'; // Неопределенное поведение
};
}
int main()
{
// Создаем нового моржа (walrus) по имени Roofus.
// sayName - это лямбда, возвращаемая makeWalrus.
auto sayName{ makeWalrus("Roofus") };
// Вызов лямбда-функции, возвращенной makeWalrus.
sayName();
return 0;
}
Вызов makeWalrus
создает временный объект std::string
из строкового литерала "Roofus". Лямбда в makeWalrus
захватывает эту временную строку по ссылке. Временная строка умирает, когда makeWalrus
возвращается, но лямбда всё еще ссылается на нее. Затем, когда мы вызываем sayName
, осуществляется доступ к висячей ссылке, что вызывает неопределенное поведение.
Обратите внимание, что это также происходит, если name
передается в makeWalrus
по значению. Переменная name
по-прежнему умирает в конце makeWalrus
, и лямбда остается с висячей ссылкой.
Предупреждение
Будьте особенно осторожны при захвате переменных по ссылке, особенно при захвате по умолчанию по ссылке. Захваченные переменные должны пережить лямбду.
Если мы хотим, чтобы захваченная переменная name
была действительной при использовании лямбды, нам нужно вместо этого захватить ее по значению (либо явно, либо с использованием захвата по умолчанию по значению).
Непреднамеренные копии изменяемых лямбда-выражений
Поскольку лямбды – это объекты, их можно копировать. В некоторых случаях это может вызвать проблемы. Рассмотрим следующий код:
#include <iostream>
int main()
{
int i{ 0 };
// Создаем новую лямбду с именем count
auto count{ [i]() mutable {
std::cout << ++i << '\n';
} };
count(); // вызываем count
auto otherCount{ count }; // создаем копию of count
// вызываем и count, и копию
count();
otherCount();
return 0;
}
Вывод программы:
1
2
2
Вместо того, чтобы печатать 1, 2, 3, этот код печатает 2 дважды. Когда мы создали otherCount
как копию count
, мы создали копию count
в его текущем состоянии. i
в count
был равен 1, поэтому i
в otherCount
также равен 1. Поскольку otherCount
является копией count
, у каждого из них есть своя собственная переменная i
.
Теперь давайте посмотрим на чуть менее очевидный пример:
#include <iostream>
#include <functional>
void invoke(const std::function<void()>& fn)
{
fn();
}
int main()
{
int i{ 0 };
// Увеличивает и печатает локальную копию i.
auto count{ [i]() mutable {
std::cout << ++i << '\n';
} };
invoke(count);
invoke(count);
invoke(count);
return 0;
}
Вывод программы:
1
1
1
Здесь проявляется та же проблема, что и в предыдущем примере, но в более непонятной форме. Когда std::function
создается с лямбда-выражением, std::function
внутренне создает копию лямбда-объекта. Таким образом, наш вызов fn()
на самом деле выполняется на копии нашей лямбды, а не на самой лямбде.
Если нам нужно передать изменяемую лямбду, и мы хотим избежать возможности создания случайных копий, есть два варианта. Один из вариантов – использовать вместо этого лямбду без захвата: в приведенном выше случае мы могли бы удалить захват и отслеживать наше состояние, используя вместо этого статическую локальную переменную. Но статические локальные переменные трудно отслеживать, и наш код становится менее читабельным. Лучше всего вообще предотвратить создание копий нашей лямбды. Но поскольку мы не можем повлиять на реализацию std::function
(или других функций или объектов стандартной библиотеки), как мы можем это сделать?
К счастью, C++ предоставляет удобный тип (как часть заголовка <functional>
), называемый std::reference_wrapper
, который позволяет нам передавать обычный тип, как если бы он был ссылкой. Для еще большего удобства можно создать std::reference_wrapper
с помощью функции std::ref()
. При оборачивании нашего лямбда-выражения в std::reference_wrapper
, всякий раз, когда кто-либо пытается сделать копию нашей лямбды, вместо этого он будет делать копию ссылки, которая будет копировать ссылку, а не фактический объект.
Вот наш обновленный код с использованием std::ref
:
#include <iostream>
#include <functional>
void invoke(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
invoke(std::ref(count));
invoke(std::ref(count));
invoke(std::ref(count));
return 0;
}
Наш результат теперь такой, как ожидалось:
1
2
3
Обратите внимание, что вывод не меняется, даже если invoke
принимает fn
по значению. std::function
не создает копию лямбды, если мы создаем ее с помощью std::ref
.
Правило
Функции стандартной библиотеки могут копировать функциональные объекты (напоминание: лямбды – это функциональные объекты). Если вы хотите предоставить лямбда-выражения с изменяемыми захваченными переменными, передайте их по ссылке, используя std::ref
.
Лучшая практика
Старайтесь вообще избегать лямбд с состояниями. Лямбда-выражения без сохранения состояния легче понять, и они не страдают от перечисленных выше проблем, а также от более опасных проблем, возникающих при добавлении параллельного выполнения.
Небольшой тест
Вопрос 1
Какие из следующих переменных могут использоваться лямбда-выражением в main
без их явного захвата?
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
Что печатает следующий код? Не запускайте код, проработайте его в уме.
#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
по значению. ИзменениеfavouriteFruit
вmain
не влияет наfavouriteFruit
в лямбда-выражении.
Вопрос 3
Мы собираемся написать небольшую игру с квадратами чисел (значениями, которые можно получить, умножив целое число на себя (1, 4, 9, 16, 25,…)).
Попросите пользователя ввести 2 числа, первое – это квадратный корень из числа, с которого нужно начинать, второе – количество чисел, которые нужно сгенерировать. Сгенерируйте случайное целое число от 2 до 4 и значения квадратов в диапазоне, выбранном пользователем. Умножьте каждое значение квадрата на случайное число. Вы можете предположить, что пользователь вводит допустимые числа.
Пользователь должен вычислить, какие числа были сгенерированы. Программа проверяет, правильно ли угадал пользователь, и удаляет угаданное число из списка. Если пользователь угадал неправильно, игра заканчивается, и программа печатает число, наиболее близкое к окончательному предположению пользователя, но только в том случае, если последнее предположение отклоняется не более чем на 4.
Вот несколько примеров запусков программы, которые помогут вам лучше понять, как работает игра:
Start where? 4
How many? 8
I generated 8 square numbers. Do you know what each number is after multiplying it by 2?
> 32
Nice! 7 number(s) left.
> 72
Nice! 6 number(s) left.
> 50
Nice! 5 number(s) left.
> 126
126 is wrong! Try 128 next time.
- Пользователь решил начать с 4 и хочет играть с 8 числами.
- Значение каждого квадрата будет умножено на 2. Число 2 было случайно выбрано программой.
- Программа генерирует 8 значений квадратов, начиная с 4 в качестве исходного значения:
16 25 36 49 64 81 100 121 - Но каждое значение умножается на 2, поэтому мы получаем:
32 50 72 98 128 162 200 242 - Теперь пользователь начинает отгадывать. Порядок ввода предположений не имеет значения:
- 32 есть в списке;
- 72 есть в списке;
- 126 нет в списке, пользователь проигрывает. В списке есть число (128), которое не более чем на 4 отличается от предположения пользователя, поэтому оно печатается.
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 numbers left.
> 16
Nice! 1 numbers left.
> 36
Nice! You found all numbers, good job!
- Пользователь решил начать с 1 и хочет играть с 3 числами.
- Значение каждого квадрата будет умножено на 4.
- Программа генерирует эти значения квадратов:
1 4 9 - Умножает на 4
4 16 36 - Пользователь угадывает все числа правильно и выигрывает игру.
Start where? 2
How many? 2
I generated 2 square numbers. Do you know what each number is after multiplying it by 4?
> 21
21 is wrong!
- Пользователь решил начать с 2 и хочет играть с 2 числами.
- Значение каждого квадрата будет умножено на 4.
- Программа генерирует эти числа:
16 36 - Пользователь вводит 21 и проигрывает. 21 недостаточно близко к какому-либо из оставшихся чисел, поэтому число не печатается.
Используйте std::find
(10.25 – Знакомство с алгоритмами стандартной библиотеки) для поиска числа в списке.
Используйте std::vector::erase
для удаления элемента, например
auto found{ std::find(/* ... */) };
// Убедимся, что элемент найден
myVector.erase(found);
Используйте std::min_element
и лямбду, чтобы найти число, наиболее близкое к предположению пользователя. std::min_element
работает аналогично std::max_element
из теста в предыдущей статье (вопрос 1).
Используйте std::abs
из <cmath>
, чтобы вычислить положительную разницу между двумя числами.
Ответ
int distance{ std::abs(5 - 3) }; // 2
#include <algorithm> // std::generate, std::find, std::min_element #include <cmath> // std::abs #include <ctime> #include <iostream> #include <random> #include <vector> using list_type = std::vector<int>; namespace config { constexpr int multiplierMin{ 2 }; constexpr int multiplierMax{ 4 }; constexpr int maximumWrongAnswer{ 4 }; } int getRandomInt(int min, int max) { static std::mt19937 mt{ static_cast<std::mt19937::result_type>(std::time(nullptr)) }; return std::uniform_int_distribution{ min, max }(mt); } // Генерирует числа (количество = count), начиная с start * start, // и умножает значение каждого квадрата на multiplier. list_type generateNumbers(int start, int count, int multiplier) { list_type numbers(static_cast<list_type::size_type>(count)); int i{ start }; for (auto& number : numbers) { number = ((i * i) * multiplier); ++i; } return numbers; } // Просит пользователя ввести начальное значение и количество, // затем вызывает generateNumbers. list_type generateUserNumbers(int multiplier) { int start{}; int count{}; std::cout << "Start where? "; std::cin >> start; std::cout << "How many? "; std::cin >> count; // Проверка ввода опущена. Все функции предполагают корректный ввод. return generateNumbers(start, count, multiplier); } int getUserGuess() { int guess{}; std::cout << "> "; std::cin >> guess; return guess; } // Ищет значение guess в numbers и удаляет его. // Возвращает true, если значение было найдено. В противном случае - false. bool findAndRemove(list_type& numbers, int guess) { auto found{ std::find(numbers.begin(), numbers.end(), guess) }; if (found == numbers.end()) { return false; } else { numbers.erase(found); return true; } } // Находит значение в numbers, наиболее близкое к guess. int findClosestNumber(const list_type& 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 printTask(list_type::size_type count, int multiplier) { std::cout << "I generated " << count << " square numbers. Do you know what each number is after multiplying it by " << multiplier << "?\n"; } // Вызывается, когда пользователь правильно угадывает число. void printSuccess(list_type::size_type numbersLeft) { std::cout << "Nice! "; if (numbersLeft == 0) { std::cout << "You found all numbers, good job!\n"; } else { std::cout << numbersLeft << " number(s) left.\n"; } } // Вызывается, когда пользователь неправильно угадывает число, // которого нет в numbers. void printFailure(const list_type& numbers, int guess) { int closest{ findClosestNumber(numbers, guess) }; std::cout << guess << " is wrong!"; if (std::abs(closest - guess) <= config::maximumWrongAnswer) { std::cout << " Try " << closest << " next time.\n"; } else { std::cout << '\n'; } } // Возвращает false, если игра окончена. true - в противном случае bool playRound(list_type& numbers) { int guess{ getUserGuess() }; if (findAndRemove(numbers, guess)) { printSuccess(numbers.size()); return !numbers.empty(); } else { printFailure(numbers, guess); return false; } } int main() { int multiplier{ getRandomInt(config::multiplierMin, config::multiplierMax) }; list_type numbers{ generateUserNumbers(multiplier) }; printTask(numbers.size(), multiplier); while (playRound(numbers)) ; return 0; }