Алгоритмы STL вместо ручных циклов: семь способов и цена каждого
Ручной for c индексом переживает все рефакторинги и прячет по одной ошибке off-by-one на брата. Разбираем семь стандартных алгоритмов STL, где каждый уместен и чем платит.

Задача простая: пройти по вектору, отфильтровать, что-то посчитать, сложить в другой контейнер. Первое, что пишет рука — for (size_t i = 0; i < v.size(); ++i). Такой цикл компилируется, работает и переживает три рефакторинга, пока кто-то не сдвинет границу на единицу и не получит либо пропущенный элемент, либо чтение за концом. Стандартная библиотека закрывает большинство таких циклов алгоритмами из <algorithm> и <numeric>: намерение видно из имени, границы не ваша забота, а ranges в C++20 убирают ещё и пару begin()/end().
Статья про то, какой алгоритм какой ручной цикл заменяет и чем каждый платит: аллокациями, лишним проходом, неочевидным поведением на пустом диапазоне. Ориентир — C++17 как база и C++20 ranges там, где они меняют картину. Компиляторы: GCC 13+, Clang 16+, MSVC 19.3x. Не рассматриваем параллельные версии с execution policy — это отдельная тема.
Семь замен и что вы отдаёте взамен
1. Сумма и свёртка — std::accumulate
Классический цикл-аккумулятор со внешней переменной sum заменяется на std::accumulate из <numeric>.
#include <numeric>
#include <vector>
std::vector<int> prices = {120, 340, 90, 15};
// сумма
int total = std::accumulate(prices.begin(), prices.end(), 0);
// произведение через лямбду-операцию
long product = std::accumulate(prices.begin(), prices.end(), 1L,
[](long acc, int x) { return acc * x; });Цена. Тип начального значения задаёт тип аккумулятора. Если написать 0 вместо 0L при суммировании больших чисел — получите переполнение int тихо, без предупреждения. Начальное значение выбирайте по типу результата, а не по типу элементов.
2. Условная сумма и подсчёт — count_if вместо ручного счётчика
Цикл «прошёл, проверил условие, увеличил счётчик» — это std::count_if.
#include <algorithm>
auto expensive = std::count_if(prices.begin(), prices.end(),
[](int p) { return p > 100; });
// expensive имеет тип difference_type, обычно ptrdiff_tЦена. Если внутри цикла вы не только считаете, но и параллельно что-то ещё делаете с элементами — count_if заставит сделать два прохода. Один — вычленять; переход обратно к циклу здесь оправдан.
3. Трансформация — transform вместо push_back в цикле
Собрать новый контейнер из старого через отображение — std::transform.
std::vector<int> with_vat;
with_vat.reserve(prices.size()); // критично, см. цену
std::transform(prices.begin(), prices.end(),
std::back_inserter(with_vat),
[](int p) { return p * 120 / 100; });Цена. back_inserter вызывает push_back на каждый элемент, и без reserve вектор переаллоцируется log-число раз. На горячем пути это заметно. Если размер результата равен размеру входа, дешевле заранее resize и писать в with_vat.begin() как в приёмник.
4. Фильтрация — copy_if, но осторожно с erase
Отобрать подмножество в новый контейнер — std::copy_if. А вот удаление на месте — отдельная ловушка.
// отбор в новый вектор
std::vector<int> cheap;
std::copy_if(prices.begin(), prices.end(),
std::back_inserter(cheap),
[](int p) { return p < 100; });
// удаление на месте: erase-remove
prices.erase(
std::remove_if(prices.begin(), prices.end(),
[](int p) { return p < 100; }),
prices.end());Цена. std::remove_if сам ничего не удаляет — он переносит «оставляемые» элементы в начало и возвращает новый логический конец. Забудете обернуть в erase — контейнер останется прежнего размера с мусором в хвосте. В C++20 есть свободная функция std::erase_if(prices, pred), которая делает обе части сразу — предпочитайте её на новых проектах.
Половина «утечек» логики в новом коде — этоremove_ifбезerase. Алгоритм честно сделал то, что обещал именем: переместил, а не удалил. Удаляет контейнер.
5. Поиск — find_if вместо цикла с break
Цикл, который ищет первый подходящий элемент и делает break, — это std::find_if.
auto it = std::find_if(prices.begin(), prices.end(),
[](int p) { return p == 90; });
if (it != prices.end()) {
// нашли, *it — элемент, it - prices.begin() — индекс
}Цена. Возвращается итератор, а не индекс и не bool. Сравнение с end() обязательно перед разыменованием — это самый частый источник UB при переходе с ручного цикла, где привыкли проверять found-флаг.
6. Проверка «все/хоть один» — all_of, any_of, none_of
Цикл, который ставит bool ok = true и сбрасывает его при первом несоответствии, заменяется на std::all_of и родственников.
bool all_positive = std::all_of(prices.begin(), prices.end(),
[](int p) { return p > 0; });
bool has_free = std::any_of(prices.begin(), prices.end(),
[](int p) { return p == 0; });Цена — граничный случай. На пустом диапазоне all_of и none_of возвращают true, а any_of — false. Это математически корректно (истинность по пустому множеству), но ломает наивные ожидания. Если пустой вход для вас означает «ошибка», проверяйте размер отдельно.
7. Сортировка по ключу — sort с проекцией
Ручной цикл вы для сортировки не пишете, но компаратор — источник багов не хуже цикла. В C++20 ranges есть проекции, которые убирают половину компараторов.
#include <algorithm> // C++20 ranges
#include <vector>
#include <string>
struct Item { std::string name; int price; };
std::vector<Item> items = /* ... */;
// вместо лямбды-компаратора — проекция на поле
std::ranges::sort(items, {}, &Item::price);
// {} — компаратор по умолчанию (less), третий аргумент — проекцияЦена. std::sort не стабилен — равные по ключу элементы могут поменяться местами. Если порядок равных важен, берите std::stable_sort, он платит дополнительной памятью. Ranges-версия требует C++20 и полной поддержки в компиляторе; на GCC она с 10, на Clang с 15, на MSVC с 19.30 — сверяйтесь с таблицей поддержки на cppreference, если проект собирается разными тулчейнами.
Что выбрать под задачу
| Что делает ручной цикл | Алгоритм | Заголовок | Главная цена |
|---|---|---|---|
| Складывает в переменную | accumulate | <numeric> | тип начального значения = тип аккумулятора |
| Считает по условию | count_if | <algorithm> | лишний проход, если нужно ещё и обработать |
| Строит новый контейнер | transform | <algorithm> | переаллокации без reserve |
| Отбирает подмножество | copy_if / erase_if | <algorithm> | remove_if не удаляет сам |
| Ищет первый подходящий | find_if | <algorithm> | возвращает итератор, нужна проверка end() |
| Проверяет условие на всех | all_of / any_of | <algorithm> | поведение на пустом диапазоне |
| Сортирует по полю | ranges::sort | <algorithm> C++20 | нестабильность, требует C++20 |
Когда ручной цикл честнее
Алгоритм STL проигрывает циклу в трёх случаях. Первый — когда за один проход нужно сделать несколько разных вещей: посчитать, отфильтровать и записать в лог. Дробить это на три алгоритма — три прохода и потеря локальности кеша. Второй — сложный break/continue с условием, зависящим от нескольких предыдущих элементов: алгоритм не хранит состояние между итерациями красиво, лямбда с захватом по ссылке выглядит хуже цикла. Третий — когда нужен индекс, а не итератор, и его пересчёт через it - begin() запутывает больше, чем помогает.
Где это ломается на практике
- Инвалидация итераторов.
transformсback_inserterв тот же вектор, по которому идёт чтение — гарантированное UB при переаллокации. Приёмник и источник должны быть разными контейнерами либо резервируйте память заранее. - Тип difference_type у count_if. Присваивание результата в
intна больших контейнерах — сужение со срезанием. Используйтеautoилиstd::ptrdiff_t. - Лямбда с дорогим захватом. Захват по значению большого объекта копирует его в каждый вызываемый объект. Захватывайте по ссылке, следя за временем жизни.
- remove_if на ассоциативных контейнерах. К
std::mapиstd::setего применять нельзя — их итераторы не движутся так, как ожидает алгоритм. Для них в C++20 естьstd::erase_if, а до него — ручной цикл сit = m.erase(it).
Что делать дальше
Начните с самого дешёвого шага: замените в существующем коде циклы-аккумуляторы на accumulate и циклы-искалки на find_if — это чистая победа без риска. Дальше включите в CI флаг -Wconversion (GCC/Clang), он ловит те самые сужения из difference_type в int. На новом коде с C++20 переходите на std::ranges-версии: они принимают контейнер целиком без пары итераторов и поддерживают проекции. Проверяйте поддержку конкретного алгоритма на cppreference — статус ranges по компиляторам там держат актуальным.
Контент-конвейер: Из одной идеи — текст, изображение, озвучка и ролик. Один заказ вместо четырёх.
Спросить за 60 ₽Источники: Algorithms library — cppreference.com, std::erase_if for containers — cppreference.com
Частые вопросы
У меня проект на C++14, ranges недоступны. Что из этого работает?
Алгоритмы STL быстрее ручного цикла?
Скомпилировалось, но remove_if не удалил элементы. Почему?
find_if вернул что-то, но при разыменовании падает. В чём дело?
all_of на пустом векторе вернул true, хотя проверять было нечего. Это баг?
Можно ли применять эти алгоритмы к std::map и std::set?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.