На цьому уроці ми розглянемо, що таке лямбда-захоплення в мові С++, як вони працюють, які є типи і як їх використовувати.
- Навіщо потрібні лямбда-захоплення?
- Введення в лямбда-захоплення
- Суть роботи лямбда-захоплень
- Захоплення змінних і const
- Захоплення по значенню
- Захоплення по посиланню
- Захоплення декількох змінних
- Захоплення за замовчуванням
- Визначення нових змінних в лямбда-захопленні
- “Висячі” захоплені змінні
- Ненавмисні копії лямбд
- Тест
Навіщо потрібні лямбда-захоплення?
На попередньому уроці ми розглядали наступний приклад:
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; } |
Давайте змінимо його так, щоб користувач сам вибирав підрядок для пошуку. Це не настільки інтуїтивно легко, як може здатися на перший погляд:
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; } |
Даний код не скомпілюється. На відміну від вкладених блоків коду, де будь-який ідентифікатор, визначений в зовнішньому блоці, доступний і у внутрішньому, лямбди в мові С++ можуть отримати доступ тільки до певних видів ідентифікаторів: глобальні ідентифікатори, об’єкти, відомі під час компіляції і зі статичною тривалістю життя. Змінна search
не відповідає жодній з цих вимог, тому лямбда не може її побачити. Ось для цього і існує лямбда-захоплення (англ. “capture clause”).
Введення в лямбда-захоплення
Поле capture clause
використовується для того, щоб надати (опосередковано) лямбді доступ до змінних з навколишньої області видимості, до яких вона зазвичай не має доступ. Все, що нам потрібно для цього зробити — перерахувати в полі capture clause
об’єкти, до яких ми хочемо отримати доступ всередині лямбди. У нашому прикладі ми хочемо надати лямбді доступ до значення змінної 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 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
(яка відноситься до блоку коду функції main()), але це не так. Так, лямбди можуть виглядати і функціонувати як вкладені блоки, але насправді вони працюють трохи по-іншому, і при цьому існує досить важлива відмінність.
Коли виконується лямбда-визначення, то для кожної захопленої змінної всередині лямбди створюється клон цієї змінної (з ідентичним ім’ям). Дані змінні-клони ініціалізуються за допомогою змінних із зовнішньої області видимості з тим же ім’ям.
Таким чином, у вищенаведеному прикладі, при створенні об’єкта лямбди, вона отримує свою власну змінну-клон з ім’ям search
. Ця змінна має таке ж значення, що і змінна search
з функції main(), тому здається ніби ми отримуємо доступ безпосередньо до змінної search
функції main(), але це не так.
Незважаючи на те, що ці змінні-клони мають одне і те ж ім’я, їх тип може відрізнятися від типу вихідної змінної (про це трохи пізніше).
Ключовий момент: Змінні, захоплені лямбдою, є клонами змінних із зовнішньої області видимості, а не фактичними «зовнішніми» змінними.
Для просунутих читачів: Коли компілятор виявляє визначення лямбди, він створює для неї визначення як для користувацького об’єкту. Кожна захоплена змінна стає елементом даних цього об’єкту. Під час виконання програми, при виявленні визначення лямбди, створюється екземпляр об’єкта лямбди і в цей момент ініціалізуються члени лямбди.
Захоплення змінних і const
За замовчуванням змінні захоплюються як константні значення. Це означає, що при створенні лямбди, вона захоплює константну копію змінної з зовнішньої області видимості, що означає, що значення цих змінних лямбда змінити не може. У наступному прикладі ми захопимо змінну 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 }; // Визначаємо лямбду всередині змінної з ім'ям 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
, всередині лямбди створюється константна змінна з таким же ім’ям і значенням. Ми не може змінити її, тому що вона має специфікатор const
. Подібна спроба зміни призведе до помилки компіляції.
Захоплення по значенню
Щоб дозволити модифікації значення змінних, які були захоплені по значенню, ми можемо помітити лямбду як mutable
. В даному контексті, ключове слово mutable видаляє специфікатор const
зі всіх змінних, захоплених по значенню:
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 }; 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
зберігається, незважаючи на виклики лямбди.
Захоплення по посиланню
Подібно до того, як функції можуть змінювати значення аргументів, переданих їм по посиланню, ми також можемо захоплювати змінні по посиланню, щоб дозволити нашій лямбді впливати на значення аргументів.
Щоб захопити змінну по посиланню, ми повинні додати знак амперсанда (&
) до імені змінної, яку хочемо захопити. На відміну від змінних, які захоплюються по значенню, змінні, які захоплюються по посиланню, не є константними (якщо тільки змінна, яку вони захоплюють, не є з самого початку const
). Якщо ви віддаєте перевагу передачі по посиланню (наприклад, для типів, які не є базовими), то замість захоплення по значенню, краще використовувати захоплення по посиланню.
Ось вищенаведений код, але вже з захопленням змінної 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 призведуть до модифікацій змінної 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() при сортуванні масиву:
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> 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
Захоплення декількох змінних
Ми можемо захопити відразу декілька змінних, розділивши їх комами. Ми також можемо використовувати як захоплення по значенню, так і захоплення по посиланню:
1 2 3 4 5 6 |
int health{ 33 }; int armor{ 100 }; std::vector<CEnemy> enemies{}; // Захоплюємо змінні health і armor по значенню, а enemies – по посиланню [health, armor, &enemies](){}; |
Захоплення за замовчуванням
Необхідність явно перераховувати змінні для захоплення іноді може бути обтяжливим. Якщо ви змінюєте свою лямбду, то ви можете забути додати або видалити захоплені змінні. На щастя, є можливість заручитися допомогою компілятора для автоматичної генерації списку змінних, які потрібно захопити.
Захоплення за замовчуванням захоплює всі змінні, згадані в лямбді. Якщо використовується захоплення за замовчуванням, то змінні, про які не йдеться в лямбді, не будуть захоплені.
Щоб захопити всі задіяні змінні по значенню, використовуйте =
в якості значення для захоплення. Щоб захопити всі задіяні змінні по посиланню, використовуйте &
в якості значення для захоплення.
Ось приклад використання захоплення за замовчуванням по значенню:
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 |
#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, &health, &armor](){}; // Заборонено, так як захоплення за замовчуванням повинне бути першим елементом в списку захоплення [armor, &](){}; |
Визначення нових змінних в лямбда-захопленні
Припустимо, що нам потрібно захопити змінну з невеликою модифікацією або оголосити нову змінну, яку видно лише в області видимості лямбди. Ми можемо це зробити, визначивши змінну в лямбда-захопленні без зазначення її типу:
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> int main() { std::array areas{ 100, 25, 121, 40, 56 }; int width{}; int height{}; std::cout << "Enter width and height: "; std::cin >> width >> height; // Ми зберігаємо змінну areas, але користувач ввів width і height. // Перш, ніж виконати операцію пошуку, ми повинні обчислити значення площі (area) 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
буде обчислена тільки один раз: під час визначення лямбди. Обчислена площа зберігається в об’єкті лямбди і однакова для кожного виклику. Якщо лямбда має модифікатор mutable
і змінює змінну, яка визначена в захопленні, то вихідне значення змінної буде перевизначене.
Порада: Ініціалізуйте змінні в захопленні тільки в тому випадку, якщо їх значення не є занадто великими і їх тип очевидний. В іншому випадку, найкраще визначити змінну поза лямбдою, а потім захопити її.
“Висячі” захоплені змінні
Змінні захоплюються в точці визначення лямбди. Якщо змінна, захоплена по посиланню, припиняє своє існування до припинення існування лямбди, то лямбда залишається з “висячим” посиланням:
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> // Функція повертає лямбду auto makeWalrus(const std::string& name) { // Захоплення змінної name по посиланню і повернення лямбди return [&]() { std::cout << "I am a walrus, my name is " << name << '\n'; // невизначена поведінка }; } int main() { // Створюємо новий об'єкт з ім'ям Roofus. // sayName є лямбдою, яка повертається функцією makeWalrus() auto sayName{ makeWalrus("Roofus") }; // Виклик лямбди, яку повертає функція makeWalrus() sayName(); return 0; } |
Виклик функції makeWalrus() створює тимчасовий об’єкт std::string зі строкового літералу "Roofus"
. Лямбда в функції makeWalrus() захоплює тимчасовий рядок по посиланню. Цей рядок знищується при виконанні повернення makeWalrus(), але при цьому лямбда все ще посилається на нього. Потім, коли ми викликаємо sayName(), відбувається спроба доступу до “висячого” посилання, що загрожує нам невизначеними результатами.
Зверніть увагу, це також відбувається, якщо змінна name
передається в функцію makeWalrus() по значенню. Змінна name
все одно припинить своє існування в кінці роботи функції makeWalrus(), і лямбда залишиться з “висячим” посиланням.
Попередження: Будьте особливо обережні при захопленні змінних по посиланню, особливо при зазначеному захопленні за замовчуванням по посиланню. Захоплені змінні повинні існувати довше, ніж сама лямбда.
Якщо ми хочемо, щоб захоплена змінна name
була валідна, коли використовується лямбда, то нам потрібно захопити дану змінну по значенню (або явно, або за допомогою захоплення за замовчуванням по значенню).
Ненавмисні копії лямбд
Оскільки лямбди є об’єктами, то їх можна копіювати. У деяких випадках це може викликати проблеми. Розглянемо наступний код:
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 auto count{ [i]() mutable { std::cout << ++i << '\n'; } }; count(); // звертаємося до count auto otherCount{ count }; // створюємо копію count // Звертаємося до count і до копії count count(); otherCount(); return 0; } |
Результат виконання програми:
1
2
2
Замість виводу 1 2 3
програма двічі виводить число 2
. Створюючи об’єкт otherCount
, як копію об’єкта count
, ми копіюємо його поточний стан. Значенням змінної i
, яка належить об’єкту count
, є 1
і значенням змінної i
, яка належить об’єкту otherCount
, так само є 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 invoke(const std::function<void(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
, то він всередині себе створює копію лямбда-об’єкта. Таким чином, наш виклик fn()
фактично виконується при використанні копії лямбди, а не самої лямбди.
Якщо нам потрібно передати модифіковану лямбду, і при цьому ми хочемо уникнути ненавмисного копіювання, то є два варіанти вирішення даної проблеми. Один з них — використати лямбду, яка не містить захоплень. У прикладі, наведеному вище, ми могли б видалити захоплення і відстежувати наш стан, використовуючи статичну локальну змінну. Але статичні локальні змінні можуть бути складні для відслідковування і роблять наш код менш читабельним. Кращий варіант — це з самого початку не допускати можливості копіювання нашої лямбди. Але, оскільки ми не можемо вплинути на реалізацію std::function
(або будь-якої іншої функції або об’єкту зі Стандартної бібліотеки С++), як ми можемо це зробити?
На щастя, C++ надає тип std::ref (як частина заголовку functional), який дозволяє нам передавати звичайний тип, наче це посилання. Обгортаючи нашу лямбду в std::ref
щоразу, коли хто-небудь намагається зробити копію нашої лямбди, він буде робити копію посилання, а не фактичного об’єкта.
Ось наш оновлений код з використанням 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> void invoke(const std::function<void(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() без їх явного захоплення?
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; } |
Відповідь №1
Змінна | Використання без явного захоплення |
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; } |
Відповідь №2
Результат виконання програми:
I like grapes
Лямбда printFavoriteFruit
захоплює favoriteFruit
по значенню. Модифікація змінної favoriteFruit
в функції main() ніяк не впливає на модифікацію змінної favoriteFruit
в лямбді.
Завдання №3
Ми збираємося написати невелику гру з квадратами чисел.
Суть гри:
Попросіть користувача ввести 2 числа: перше — стартове число, яке потрібно піднести до квадрату, друге — кількість чисел, які потрібно піднести до квадрату.
Згенеруйте випадкове ціле число від 2
до 4
і піднесіть до квадрату вказану користувачем кількість чисел, починаючи зі стартового числа.
Помножте кожне піднесене до квадрату число на раніше згенероване число (від 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()
для пошуку числа в списку.
Використовуйте std::vector::erase()
для видалення елементу, наприклад:
1 2 3 4 5 |
auto found{ std::find(/* ... */) }; // Переконайтеся, що елемент був знайдений myVector.erase(found); |
Використовуйте std::min_element()
і лямбду, щоб знайти найбільш близьке до припущення користувача число. std::min_element()
працює аналогічно std::max_element()
з тесту попереднього уроку.
Використовуйте std::abs()
з заголовку cmath, щоб обчислити різницю між двома числами:
1 |
int distance{ std::abs(5 - 3) }; // 2 |
Відповідь №3
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 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
#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) { if (auto found{ std::find(numbers.begin(), numbers.end(), guess) }; 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; } |