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().
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ