Алгоритмы STL в C++: что реально стоит применять на практике
Ручной цикл for с индексом переписывают на std::ranges не ради моды, а потому что он молча ломается на пустых контейнерах и off-by-one. Разбираем, какие алгоритмы STL экономят время, а какие тянут за собой сюрпризы.

Типичная задача: найти в векторе первый элемент больше порога, посчитать сколько таких, а остальные удалить. Очевидное решение — три цикла for (size_t i = 0; i < v.size(); ++i). Оно работает, пока кто-то не поменяет < на <=, не забудет проверить пустой контейнер или не удалит элемент прямо в цикле, сдвинув индексы. Алгоритмы из <algorithm> и <ranges> закрывают ровно эти классы ошибок — но у каждого своя цена. Разберём, что советуют применять и где подход кусается.
Всё ниже — для C++20 и новее (GCC 11+, Clang 14+, MSVC 19.29+). Ranges-версии алгоритмов появились в C++20; часть, вроде std::ranges::to, только в C++23, и это оговорено отдельно.
Почему сырой цикл — плохой выбор по умолчанию
Ручной цикл смешивает три вещи: обход, условие и действие. Читающий код каждый раз заново распознаёт намерение. Алгоритм с говорящим именем сообщает намерение прямо: std::count_if — «считаем по условию», std::remove_if — «убираем по условию». Это не эстетика, это скорость чтения ревью и снижение шанса on-by-one.
Правило Саттера и Александреску из «C++ Coding Standards» звучит коротко: «Предпочитайте алгоритмы циклам». За двадцать лет оно не устарело — устарел только синтаксис вызова.
Но замена работает не механически. Три места, где алгоритм STL проигрывает циклу, разберём в конце. Сначала — что применять.
Четыре подхода к перебору с условием
1. Классический алгоритм с итераторами
#include <algorithm>
#include <vector>
std::vector<int> values{4, 8, 15, 16, 23, 42};
// Сколько элементов больше 15
auto big = std::count_if(values.begin(), values.end(),
[](int x) { return x > 15; });
// big == 3
Работает с C++98 (в форме count_if), лямбды — с C++11. Минус: пара begin()/end() руками, легко передать итераторы от разных контейнеров и получить UB без предупреждения компилятора.
2. Ranges-версия (C++20)
#include <algorithm>
#include <ranges>
#include <vector>
std::vector<int> values{4, 8, 15, 16, 23, 42};
auto big = std::ranges::count_if(values, [](int x) { return x > 15; });
// big == 3
Контейнер передаётся целиком, перепутать begin и end невозможно. Плюс поддержка проекций — можно считать по полю структуры, не описывая лямбду руками:
struct Order { int id; double sum; };
std::vector<Order> orders = /* ... */;
// Считаем заказы дороже 1000, проекция вытаскивает поле sum
auto expensive = std::ranges::count_if(
orders,
[](double s) { return s > 1000.0; },
&Order::sum);
3. Ranges views — ленивая цепочка
#include <ranges>
#include <vector>
std::vector<int> values{4, 8, 15, 16, 23, 42};
// Отфильтровать и удвоить — без промежуточного вектора
auto pipeline = values
| std::views::filter([](int x) { return x > 15; })
| std::views::transform([](int x) { return x * 2; });
for (int v : pipeline) {
// 32, 46, 84
}
Views ничего не вычисляют, пока по ним не пройтись. Промежуточные контейнеры не создаются. Это же и источник главной ловушки — про висячие ссылки в разделе про поломки.
4. Сырой цикл — когда он всё-таки уместен
Если тело обхода тянет несколько разнородных действий с ранним break, накоплением в две переменные и побочными эффектами, попытка выразить это одним алгоритмом порождает нечитаемую лямбду. Тут честный for честнее.
Сравнение подходов
| Подход | Версия C++ | Плюс | Цена |
|---|---|---|---|
| Итераторный алгоритм | C++11 | работает везде, минимум зависимостей | руками begin/end, риск смешать итераторы |
| ranges::алгоритм | C++20 | контейнер целиком, проекции | нужен свежий компилятор |
| views цепочка | C++20 | без промежуточных копий, ленивость | висячие ссылки, отладка сложнее |
| Сырой цикл for | любая | любая нестандартная логика | off-by-one, дублирование намерения |
Ходовой набор алгоритмов, который окупается
- std::sort / std::ranges::sort — сортировка, часто в связке с проекцией по полю.
- std::find_if / count_if — поиск и подсчёт по условию, самая частая замена цикла.
- std::remove_if + erase — удаление по условию (см. ловушку ниже), в C++20 короче —
std::erase_if. - std::accumulate (из
<numeric>) — свёртка в одно значение: сумма, произведение, конкатенация. - std::transform — отображение элементов один в один.
- std::any_of / all_of / none_of — проверка «хоть один / все / ни один» с ранним выходом.
- std::min_element / max_element — экстремум, возвращает итератор, а не значение (частая ошибка новичков).
Erase-remove: главный источник багов
Самая известная ловушка STL. std::remove_if ничего не удаляет — он переносит «выживших» в начало контейнера и возвращает итератор на новый конец. Размер вектора не меняется. Забыть второй шаг — значит оставить мусор в хвосте:
#include <algorithm>
#include <vector>
std::vector<int> values{4, 8, 15, 16, 23, 42};
// НЕВЕРНО: размер не изменился, хвост содержит мусор
std::remove_if(values.begin(), values.end(),
[](int x) { return x > 15; });
// ВЕРНО (классика): remove_if + erase
values.erase(
std::remove_if(values.begin(), values.end(),
[](int x) { return x > 15; }),
values.end());
// ВЕРНО и короче (C++20):
std::erase_if(values, [](int x) { return x > 15; });
Совет практики: в C++20 и новее используйте std::erase_if — идиому remove-erase почти не нужно писать руками.
Где подход ломается
Висячие ссылки во views
View хранит ссылку на источник, а не копию. Если источник — временный объект, view переживёт его и укажет в никуда:
// ОПАСНО: getVector() вернул временный вектор, view держит на него ссылку
auto view = getVector() | std::views::filter(pred);
// к моменту обхода временный объект уже уничтожен — UB
Держите источник живым явной переменной, а не связывайте цепочку с временным значением.
min_element на пустом диапазоне
std::min_element на пустом контейнере вернёт end(). Разыменовать его — UB. Проверяйте, что итератор не равен end(), до обращения к значению.
Инвалидация итераторов
Модификация контейнера внутри алгоритма (кроме предусмотренных remove-подобных) инвалидирует итераторы. std::vector при перевыделении памяти делает недействительными все итераторы — цикл с добавлением элементов в тот же вектор через сохранённый итератор упадёт.
accumulate и тип аккумулятора
Тип результата std::accumulate задаётся типом начального значения. std::accumulate(v.begin(), v.end(), 0) для вектора double накопит в int и потеряет дробную часть. Начальное значение должно быть 0.0.
Когда что выбирать
- Компилятор поддерживает C++20 — по умолчанию
std::ranges::*: короче вызов, невозможно перепутать итераторы. - Нужно преобразовать поток данных без промежуточных копий и результат обходится один раз —
views, но следите за временем жизни источника. - Проект на C++11/14/17 — классические итераторные алгоритмы, они всё те же.
- Логика тела не сводится к одному предикату (ранний выход, два аккумулятора, побочные эффекты) — оставьте
for, не насилуйте лямбду.
Что делать дальше
Прогоните текущий код через простую проверку: каждый цикл for с индексом и условием — кандидат на ranges::count_if, find_if или erase_if. Включите -Wall -Wextra и статический анализатор (clang-tidy с проверками из группы modernize-* подсказывает замены циклов на алгоритмы). Для C++23 посмотрите std::ranges::to — он материализует view обратно в контейнер без ручного цикла копирования; проверьте поддержку в вашей версии стандартной библиотеки, она появилась не сразу. Инвалидация итераторов в ассоциативных контейнерах — отдельная большая тема, здесь мы её не разворачивали.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: Algorithms library — cppreference.com, Ranges library — cppreference.com
Частые вопросы
У меня проект на C++17, ranges недоступны. Что делать?
Почему после remove_if размер вектора не изменился?
Алгоритм скомпилировался, но программа падает или ведёт себя странно — где искать?
Алгоритмы STL медленнее ручного цикла?
std::accumulate возвращает целое число, хотя вектор из double. Почему?
Стоит ли переписывать весь старый код на ranges?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.