Menu

Алгоритмы STL в C++: практическое руководство с примерами

Используйте стандартные алгоритмы C++ - find, count_if, transform, accumulate, remove - чтобы делать реальную работу над диапазонами без написания циклов вручную, а также разберитесь с подводными камнями пары итераторов и идиомы erase-remove.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Циклы, которые не нужно писать

На предыдущей странице вы видели, что каждый контейнер выдаёт итераторы - лёгкие курсоры с begin() и end(). Именно ради этой абстракции и существуют стандартные алгоритмы. Вместо того чтобы каждый раз писать обычный цикл for, когда нужно найти, посчитать или преобразовать данные, вы вызываете именованную функцию из <algorithm> и передаёте ей диапазон.

Диапазон - это всего лишь два итератора: откуда начинать и на единицу дальше того, где остановиться. Поскольку каждый алгоритм говорит на одном и том же языке итераторов, один и тот же find работает с vector, string или обычным массивом.

Шаблон, который стоит усвоить: алгоритм поиска возвращает итератор end(), чтобы обозначить «ничего не найдено». Всегда сравнивайте результат с end() перед разыменованием - разыменование end() является неопределённым поведением.

Подсчёт и проверка с помощью предикатов

Многие алгоритмы принимают предикат - функцию (обычно лямбду), которая возвращает bool для каждого элемента. count_if считает совпадения; all_of, any_of и none_of отвечают на вопросы «да/нет» обо всём диапазоне.

std::count (без _if) - более простой родственник, который считает точное значение, а не условие. Переходите к версиям с предикатом, как только ваша проверка становится «всё, что подходит под правило», а не «именно это значение».

Преобразование и свёртка диапазона

Две рабочие лошадки покрывают большую часть обработки данных: std::transform отображает каждый элемент через функцию, а std::accumulate (из <numeric>, а не из <algorithm>) сворачивает диапазон до единственного значения.

transform записывает свои результаты через выходной итератор. Распространённая и опасная ошибка - направить вывод в пустой vector: алгоритм предполагает, что место уже есть, и пишет за концом. Либо заранее задайте размер приёмника, либо используйте back_inserter, чтобы каждый результат добавлялся через push_back.

accumulate начинает с начального значения и комбинирует элементы слева направо. Тип начального значения имеет значение: передайте 0 (это int), и сумма будет вычисляться в int, что может привести к переполнению или усечению.

Если бы вы написали accumulate(prices.begin(), prices.end(), 0) с затравкой int, каждое сложение происходило бы в int и копейки бы исчезли. Тип затравки незаметно определяет тип результата.

Идиома erase-remove

Вот подводный камень, который удивляет всех. std::remove не удаляет ничего из контейнера. У алгоритмов есть только итераторы, поэтому они не могут изменить размер контейнера - они даже не знают, что контейнер вообще существует. На самом деле remove перемещает все сохраняемые элементы к началу, оставляя хвост в неопределённом состоянии, и возвращает итератор на новый логический конец.

// один remove оставляет размер неизменным - вот в чём баг:
remove(v.begin(), v.end(), 0);  // возвращает итератор, который вы проигнорировали
// v всё ещё имеет исходный размер; хвост - мусор

Чтобы по-настоящему удалить элементы, вы сочетаете remove с erase контейнера, поэтому это и называется идиомой erase-remove:

Используйте remove_if для предиката вместо точного значения. В C++20 можно полностью обойтись без этого танца с помощью свободных функций std::erase / std::erase_if, которые делают оба шага за вас: erase(v, 0);.

Итераторы переживают свою действительность на ваш страх и риск

Поскольку алгоритмы возвращают итераторы, эти итераторы подчиняются тем же правилам инвалидации, с которыми вы познакомились на предыдущей странице. Сохранение итератора, а затем изменение контейнера - push_back, вызывающий перевыделение памяти, или erase - может оставить сохранённый итератор повисшим, и его использование является неопределённым поведением.

auto it = find(v.begin(), v.end(), 16);
v.push_back(99);   // может перевыделить хранилище v
cout << *it;       // BUG: `it` теперь может указывать на освобождённую память

Безопасная привычка: используйте итератор, возвращённый алгоритмом, немедленно, до любой операции, которая может изменить размер или перевыделить контейнер. Если вам нужно изменить контейнер на основе результата, захватите вместо этого индекс (it - v.begin()), поскольку индексы переживают перевыделение.

Далее: сортировка

Теперь вы увидели поиск, подсчёт, преобразование и свёртку, но один алгоритм достаточно важен, чтобы заслужить отдельную страницу. std::sort переупорядочивает диапазон на месте, и как только данные отсортированы, открывается целое семейство более быстрых алгоритмов, работающих только на отсортированных данных (binary_search, lower_bound, equal_range). Дальше мы углубимся в сортировку: как задать собственный компаратор, в чём разница между sort и stable_sort, и каким правилам должна подчиняться ваша функция сравнения, чтобы избежать неопределённого поведения.

Часто задаваемые вопросы

Что такое заголовок <algorithm> в C++?

<algorithm> - это заголовок стандартной библиотеки, содержащий обобщённые функции, такие как std::find, std::sort, std::count_if и std::transform. Они работают над диапазонами, описанными парой итераторов (обычно begin() и end()), поэтому один и тот же алгоритм работает с vector, array, string или любым контейнером, предоставляющим итераторы.

Как проверить, существует ли значение в vector в C++?

Используйте std::find: auto it = find(v.begin(), v.end(), target);. Если it == v.end(), значения нет; иначе it указывает на первое совпадение. Чтобы проверить условие вместо точного значения, используйте std::any_of с предикатом.

Почему std::remove на самом деле не удаляет элементы из моего контейнера?

Алгоритмы видят только итераторы, а не контейнер, поэтому std::remove не может его уменьшить - он сдвигает сохраняемые элементы к началу и возвращает итератор на новый логический конец. Чтобы физически отбросить остаток, нужно дополнить его вызовом v.erase(...) (идиома erase-remove).

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ