JavaRush /Курсы /C++ SELF /find_if, count_if, any_of/all_of: проверки без циклов

find_if, count_if, any_of/all_of: проверки без циклов

C++ SELF
21 уровень , 1 лекция
Открыта

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. Это не только быстрее, но и логически «честнее»: мы не обязаны смотреть на всё, если ответ уже понятен.

Небольшая таблица смысла

Алгоритм Вопрос к данным Когда может остановиться раньше
find_if
«Где первый подходящий?» Да (нашёл — остановился)
count_if
«Сколько подходящих?» Нет (нужно пройти всё)
any_of
«Есть хоть один подходящий?» Да (нашёл — остановился)
all_of
«Все подходят?» Да (нашёл нарушителя — стоп)

Пример 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().

1
Задача
C++ SELF, 21 уровень, 1 лекция
Недоступна
Тёплые дни
Тёплые дни
1
Задача
C++ SELF, 21 уровень, 1 лекция
Недоступна
Цифра в пароле
Цифра в пароле
1
Задача
C++ SELF, 21 уровень, 1 лекция
Недоступна
Первый кратный
Первый кратный
1
Задача
C++ SELF, 21 уровень, 1 лекция
Недоступна
Проверка каталога
Проверка каталога
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ