1. Вступ
Коли ви тільки починаєте програмувати, цикл for здається універсальною суперсилою: хочете знайти елемент — пишете цикл, хочете порахувати — пишете цикл, хочете перевірити умову — знову цикл. Це нормально. Проблема починається тоді, коли ви помічаєте, що 70% коду — це не суть завдання, а технічна рутина: створити лічильник, додати прапорець, не забути break, коректно опрацювати порожній контейнер.
Алгоритми STL дають змогу говорити з кодом трохи більш «людською» мовою:
- «Знайди перший елемент, який підходить» → std::find_if
- «Порахуй, скільки підходить» → std::count_if
- «Чи є хоча б один відповідний?» → std::any_of
- «Чи всі підходять?» → std::all_of
І це не «магія». Усередині однаково працює цикл. Просто його вже написали один раз, перевірили багато людей і дали йому таку назву, що код читається майже як речення. А в програмуванні це рідкісна розкіш.
Але спершу нам потрібен ще один термін.
2. Предикат: функція, яка відповідає «так/ні»
Зараз буде слово, яке звучить наче імʼя якогось давнього бога: предикат. Але на практиці все дуже просто: це функція, яка отримує один елемент і повертає bool.
Тобто предикат — це просто «перевірка». Наприклад: «число парне?», «завдання виконано?», «рядок не порожній?». У коді зазвичай це виглядає так: bool pred(const T& x).
Мініприклад: предикат для int
bool is_even(int x) {
return x % 2 == 0;
}
Мініприклад: предикат для struct Task
Припустімо, у нас є завдання:
#include <string>
struct Task {
int id{};
std::string title;
bool done{};
};
Тоді предикат «завдання виконано?» буде таким:
bool is_done(const Task& t) {
return t.done;
}
Зверніть увагу на один приємний момент: імʼя is_done вже саме пояснює, що відбувається. Це майже як коментар, тільки краще, бо коментарі інколи брешуть, а функція — рідко.
3. std::find_if: «де перший відповідний?»
Тепер переходимо до першої справжньої суперздібності. Дуже часто вам потрібно не «порахувати» і не «перевірити», а саме знайти. Наприклад: знайти завдання за id, знайти перше виконане завдання, знайти перший порожній рядок, знайти перше число, більше за 100.
std::find_if якраз розвʼязує завдання «знайди перший елемент у діапазоні, для якого предикат повертає true».
Потрібний заголовковий файл:
#include <algorithm>
Базова форма
Якщо у вас є контейнер v, то «весь контейнер» — це діапазон [v.begin(), v.end()).
auto it = std::find_if(v.begin(), v.end(), pred);
Результат такий: it — це ітератор. Він або вказує на знайдений елемент, або дорівнює v.end(), що й означає «не знайдено».
Приклад 1: знайти перше виконане завдання
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
struct Task { int id{}; std::string title; bool done{}; };
bool is_done(const Task& t) { return t.done; }
int main() {
std::vector<Task> tasks{{1, "Read", false}, {2, "Code", true}};
auto it = std::find_if(tasks.begin(), tasks.end(), is_done);
if (it != tasks.end()) std::cout << it->title << '\n'; // Code
}
Тут важливі дві речі. По-перше, ми перевіряємо it != tasks.end(), перш ніж використовувати it->title. По-друге, it->title читається дуже природно: «у знайденого завдання взяти title».
Приклад 2: знайти завдання за id через предикат
Предикат може перевіряти будь-що, але памʼятайте: ми ще не дійшли до лямбд, тому зробимо предикат, який шукає фіксований id, наприклад 2. Так, це трохи менш гнучко, зате цілком чесно в межах поточних знань.
bool has_id_2(const Task& t) {
return t.id == 2;
}
І використання:
auto it = std::find_if(tasks.begin(), tasks.end(), has_id_2);
if (it != tasks.end()) std::cout << it->title << '\n';
Пізніше, але не сьогодні, ми навчимося зручно й красиво задавати умови «з параметром». А поки нам важливо зрозуміти саму механіку: find_if шукає за правилом, яке ви йому дали.
4. std::count_if: «скільки елементів підходить?»
Після «знайти» зазвичай хочеться «порахувати». І тут новачки майже завжди пишуть один і той самий цикл: int cnt = 0, а потім for (...) if (...) ++cnt. Це нормальна техніка, але вона повторюється буквально в кожному проєкті, і через це код починає нагадувати «день бабака».
std::count_if робить те саме, але коротше: він повертає кількість елементів, для яких предикат істинний.
Суттєва відмінність від find_if: find_if може зупинитися на першому відповідному елементі, а count_if мусить пройти весь діапазон, бо інакше не дізнається точної кількості.
Приклад 1: порахувати виконані завдання
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
struct Task { int id{}; std::string title; bool done{}; };
bool is_done(const Task& t) { return t.done; }
int main() {
std::vector<Task> tasks{{1, "Read", false}, {2, "Code", true}, {3, "Sleep", true}};
int done_count = std::count_if(tasks.begin(), tasks.end(), is_done);
std::cout << done_count << '\n'; // 2
}
Намір видно одразу: count_if(..., is_done) — «порахуй, скільки виконаних».
Приклад 2: порахувати непорожні заголовки
bool has_non_empty_title(const Task& t) {
return !t.title.empty();
}
Використання:
int ok_titles = std::count_if(tasks.begin(), tasks.end(), has_non_empty_title);
std::cout << ok_titles << '\n';
Це вже схоже на реальну перевірку якості даних: «скільки завдань у нас узагалі мають нормальний заголовок».
5. std::any_of і std::all_of: перевірки «чи є?» і «чи всі?»
Дуже поширений запит до даних звучить так: «Чи є хоча б один елемент із такою властивістю?» або, навпаки, «Чи всі елементи нормальні?». Це можна розвʼязати циклом із прапорцем, можна — через find_if, але в STL для цього є спеціалізовані алгоритми. Вони часто читаються краще, бо виражають думку прямо.
std::any_of(first, last, pred) повертає true, якщо є хоча б один елемент, для якого pred істинний.
std::all_of(first, last, pred) повертає true, якщо для всіх елементів pred істинний.
І приємний бонус: обидва алгоритми можуть завершитися раніше, ніж дійдуть до кінця діапазону. any_of зупиниться на першому true, all_of — на першому false. Це не лише швидше, а й цілком логічно: ми не зобовʼязані переглядати все, якщо відповідь уже зрозуміла.
Невелика таблиця сенсу
| Алгоритм | Запитання до даних | Коли може зупинитися раніше |
|---|---|---|
|
«Де перший відповідний?» | Так (знайшов — зупинився) |
|
«Скільки відповідних?» | Ні (потрібно пройти все) |
|
«Чи є хоч один відповідний?» | Так (знайшов — зупинився) |
|
«Чи всі підходять?» | Так (знайшов порушення — стоп) |
Приклад 1: чи є хоча б одне виконане завдання?
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
struct Task { int id{}; std::string title; bool done{}; };
bool is_done(const Task& t) { return t.done; }
int main() {
std::vector<Task> tasks{{1, "Read", false}, {2, "Code", false}};
bool has_done = std::any_of(tasks.begin(), tasks.end(), is_done);
std::cout << has_done << '\n'; // 0
}
Приклад 2: чи всі id додатні?
bool id_is_positive(const Task& t) {
return t.id > 0;
}
Використання:
bool ok = std::all_of(tasks.begin(), tasks.end(), id_is_positive);
std::cout << ok << '\n';
Така перевірка — це вже маленький «контракт даних»: якщо id раптом стане 0 або відʼємним, ви дізнаєтеся про це завдяки явній перевірці, а не через дивні баги пізніше.
6. Практичний мініприклад TaskBox
Тепер зберемо все в маленький «каркас застосунку». Це ще не повноцінний менеджер завдань — ми поки не робимо складне введення чи меню, — але це вже програма, де дані зберігаються у вигляді моделей (struct), а операції над ними винесені в окремі функції. І найголовніше: пошук, підрахунок і перевірки ми виконуємо алгоритмами.
Крок 1: модель і тестові дані
#include <string>
#include <vector>
struct Task {
int id{};
std::string title;
bool done{};
};
std::vector<Task> make_demo_tasks() {
return {{1, "Read docs", false}, {2, "Write code", true}, {3, "", false}};
}
Зауважте: одне завдання спеціально має порожній title. Це наш «вбудований тест», щоб перевірки були не декоративними.
Крок 2: предикати
bool is_done(const Task& t) {
return t.done;
}
bool title_not_empty(const Task& t) {
return !t.title.empty();
}
bool id_is_positive(const Task& t) {
return t.id > 0;
}
Тут хочеться пожартувати, що предикати — це «охоронці на вході до клубу»: хто проходить, того й пускаємо. Але якщо серйозно, хороші імена цих функцій роблять код майже самодокументованим.
Крок 3: знайти перше виконане завдання
#include <algorithm>
#include <vector>
std::vector<Task>::const_iterator find_first_done(const std::vector<Task>& tasks) {
return std::find_if(tasks.begin(), tasks.end(), is_done);
}
Тут ми повертаємо ітератор. Чому const_iterator? Тому що ми не збираємося змінювати завдання через цей результат. Це акуратний контракт: «шукаю, але не редагую».
Крок 4: порахувати виконані завдання
#include <algorithm>
#include <vector>
int count_done(const std::vector<Task>& tasks) {
return static_cast<int>(std::count_if(tasks.begin(), tasks.end(), is_done));
}
Чому тут static_cast<int>? count_if повертає тип на кшталт std::ptrdiff_t (у загальному випадку), а нам для простого виведення й у навчальному прикладі зручний int. Це нормальна практика, якщо ви впевнені, що у вас не мільярди завдань.
Крок 5: перевірки «чи є?» і «чи всі?»
#include <algorithm>
#include <vector>
bool has_any_done(const std::vector<Task>& tasks) {
return std::any_of(tasks.begin(), tasks.end(), is_done);
}
bool all_titles_ok(const std::vector<Task>& tasks) {
return std::all_of(tasks.begin(), tasks.end(), title_not_empty);
}
Якщо ви дивитеся на ці функції й думаєте: «Надто просто» — чудово. Простота в перевірках і статистиці зазвичай означає менше багів.
Крок 6: поєднуємо все в main() і друкуємо результати
#include <iostream>
int main() {
auto tasks = make_demo_tasks();
std::cout << "done_count=" << count_done(tasks) << '\n'; // done_count=1
std::cout << "has_any_done=" << has_any_done(tasks) << '\n'; // has_any_done=1
std::cout << "all_titles_ok=" << all_titles_ok(tasks) << '\n'; // all_titles_ok=0
}
Уже зараз це корисна програма: вона показує «здоровʼя» даних. І ми написали її без жодного ручного циклу в логіці перевірок. Цикли, звісно, нікуди не зникли — просто тепер вони акуратно «запаковані» в STL.
Як обирати алгоритм за запитанням
Дуже поширена помилка новачка — обрати інструмент лише тому, що він «схожий». Наприклад, замість any_of використати count_if(...) > 0. Це працюватиме, але гірше передаватиме сенс і часто буде менш ефективним, бо count_if мусить пройти весь діапазон, а any_of може завершитися раніше.
Корисна звичка: перш ніж писати код, сформулюйте запитання до даних звичайною людською мовою. Якщо запитання звучить як «знайди», майже завжди першим кандидатом буде find_if. Якщо звучить як «скільки», беріть count_if. Якщо звучить як «чи є», беріть any_of. Якщо звучить як «чи всі», беріть all_of.
І так, інколи правильна відповідь — «звичайний цикл». Наприклад, якщо ви одночасно і рахуєте, і збираєте список знайдених, і щось друкуєте по дорозі. Але сьогодні ми якраз тренуємося розпізнавати ситуації, де алгоритм виражає думку простіше.
7. Типові помилки
Помилка №1: розіменувати результат find_if, не перевіривши його на end().
std::find_if у випадку «не знайдено» повертає ітератор, рівний end(). Якщо після цього написати *it або it->field, ви в найкращому разі отримаєте падіння програми, а в найгіршому — дивну поведінку. Правильний шаблон тут завжди той самий: спочатку if (it != v.end()), і лише всередині — доступ до елемента.
Помилка №2: використовувати count_if там, де потрібна відповідь «чи є?».
Коли пишуть std::count_if(...) > 0, це виглядає логічно, але гірше передає сенс. Наступний читач коду бачить «порахувати» й думає, що вам важлива точна цифра, хоча ви просто перевіряєте факт існування. До того ж count_if пройде весь контейнер, навіть якщо відповідний елемент був першим. Для «чи є?» краще використовувати std::any_of або find_if.
Помилка №3: предикат із побічними ефектами (друк, зміна даних).
Предикат задуманий як чиста перевірка: «так/ні». Якщо всередині предиката ви друкуєте в консоль або, що гірше, змінюєте елементи, то робите поведінку алгоритму менш передбачуваною й ускладнюєте налагодження. Особливо підступно це працює в перевірках, які можуть завершуватися раніше (any_of/all_of): частину елементів алгоритм може навіть не перевірити, і ви отримаєте уривчастий друк або неповні зміни.
Помилка №4: плутанина «ітератор — це індекс».
Ітератор — не число. З ним не можна поводитися як з індексом і просто «додати 1», як ви робили з int i. Так, у ітератора є ++it, але це інша сутність: не позиція, а радше «вказівник на елемент». Якщо вам потрібно отримати номер елемента, це робиться окремо — і не завжди це взагалі потрібно. Поки що тримайте в голові просте правило: алгоритми повертають ітератори, і порівнювати їх потрібно з end(), а не з якимись числами.
Помилка №5: порівнювати ітератори від різних контейнерів.
Інколи код виглядає так: «знайшов в одному векторі, порівняв з end() іншого». Компілятор інколи навіть пропускає таке (залежно від типів), але логічно це нісенітниця. Ітератор «живе» у своєму контейнері. Якщо ви зробили auto it = find_if(a.begin(), a.end(), ...), то перевіряти треба it != a.end(), а не b.end().
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ