1. Зачем нужны std::ranges‑алгоритмы, если std::sort и так работает
Если вы когда-нибудь писали std::sort(v.begin(), v.end()) в двадцатый раз за вечер, то вы уже эмоционально готовы к std::ranges. Классические STL‑алгоритмы исторически принимают пару итераторов (first, last), и это нормально… пока вы не начинаете постоянно повторять одно и то же и случайно не склеиваете «начало от одного контейнера» с «концом от другого». Да, это звучит как «я случайно взял правый носок от одной пары и левый от другой», но в C++ компилятор иногда не может вас остановить.
std::ranges‑алгоритмы появились как попытка сделать код менее шумным и более «смысловым»: вы чаще передаёте контейнер целиком, а не две его «границы». Заодно библиотека постепенно обрастала удобными исправлениями и расширениями, связанными с ranges и алгоритмами. Это видно даже по истории рабочих черновиков стандарта, где регулярно проходят изменения в области ranges/алгоритмов, включая добавление новых ranges‑алгоритмов и уточнения поведения.
2. Термин “range”: что это такое по‑человечески
Слово range звучит так, будто мы сейчас будем стрелять по мишеням и обсуждать баллистику. На самом деле в C++ «range» — это объект, который можно обойти: у него есть понятные «начало» и «конец», то есть его можно превратить в итераторы begin и end. Вы уже пользовались этой идеей каждый раз, когда писали range‑for:
for (int x : v) { /* ... */ }
Range‑for внутри себя как раз и делает что-то похожее на begin(v) и end(v). В мире std::ranges мы просто начинаем чаще говорить «дай мне весь диапазон», а не «дай мне отдельно начало и конец».
Небольшая схема, чтобы закрепить, что происходит в голове у программы:
flowchart LR
A[Контейнер v] --> B["begin(v)"]
A --> C["end(v)"]
B --> D[Итерация/алгоритм]
C --> D
3. Главное отличие интерфейса: контейнер целиком вместо begin/end
Когда вы начинаете пользоваться std::ranges, первое ощущение обычно такое: «вроде то же самое, но короче». И это хороший знак. Например, сортировка.
Классический std::sort
#include <algorithm>
#include <vector>
int main() {
std::vector<int> v{4, 1, 3, 2};
std::sort(v.begin(), v.end());
}
std::ranges::sort
#include <algorithm>
#include <ranges>
#include <vector>
int main() {
std::vector<int> v{4, 1, 3, 2};
std::ranges::sort(v); // сортируем весь контейнер
}
Смысл тот же, но теперь код читает себя почти как фраза: «отсортируй v». Для новичка это особенно важно: меньше «синтаксического шума» — легче увидеть идею.
Тут полезно помнить простое правило: ranges‑алгоритмы по-прежнему лежат в <algorithm>, а <ranges> часто подключают, потому что это «мир ranges» (концепты, view‑шки, утилиты). В учебных примерах проще подключать оба заголовка и не устраивать охоту на компиляторные ошибки «а где объявлено».
Шпаргалка: классические вызовы и ranges‑вызовы
Иногда лучше один раз увидеть рядом, чем десять раз прочитать объяснение. Поэтому — короткая таблица, где видно главное: ranges‑версии часто принимают контейнер целиком.
| Задача | Классический алгоритм | Ranges‑алгоритм |
|---|---|---|
| Сортировка | |
|
| Поиск по условию | |
|
| Подсчёт | |
|
| Копирование | |
|
Обратите внимание: смысл не меняется. Меняется «форма вызова», и чаще всего — в сторону более читаемой.
4. Мини‑приложение для примеров: список задач TodoLite
Чтобы примеры не выглядели как «вакуумный сферический vector<int>», давайте продолжим наш стиль: у нас есть маленькая модель данных, и мы пишем функции для работы с ней. Полноценный интерфейс и «красивое приложение» мы сегодня не строим (нам важны алгоритмы), но кусочки кода будут складываться в один проект.
Начнём с модели Task:
#include <string>
struct Task {
int id = 0;
std::string title;
bool done = false;
int priority = 0; // чем меньше — тем важнее (да, это странно, но удобно)
};
И мини‑печать одной задачи:
#include <iostream>
#include <string>
void printTask(const Task& t) {
std::cout << "#" << t.id << " [" << (t.done ? "x" : " ") << "] "
<< t.title << " (p=" << t.priority << ")\n";
// пример вывода: #3 [ ] Buy milk (p=2)
}
5. Поиск: std::ranges::find и std::ranges::find_if
Когда люди слышат «ranges‑алгоритмы», иногда ожидают, что они магически перестанут возвращать итераторы и начнут возвращать «понятные ответы». Но нет: философия STL сохраняется. Многие алгоритмы поиска возвращают итератор на найденный элемент. Если не нашли — возвращают end().
Важное отличие: теперь алгоритм знает ваш контейнер и может сам взять у него end(). Но сравнивать результат всё равно нужно с v.end(), потому что итератор относится к конкретному контейнеру.
Поиск по условию (find_if)
#include <algorithm>
#include <iostream>
#include <ranges>
#include <vector>
int main() {
std::vector<int> v{3, 7, 10, 5};
auto it = std::ranges::find_if(v, [](int x) { return x % 2 == 0; });
if (it != v.end()) {
std::cout << *it << "\n"; // 10
}
}
Смысл проверки «не найдено» остался прежним: it == v.end().
Поиск задачи по id и проекция
Теперь сделаем реально полезную функцию для TodoLite: найти задачу по идентификатору. Мы можем использовать std::ranges::find, но у нас контейнер не из int, а из Task.
В ranges‑интерфейсе есть очень приятная фича: projection (проекция). Проекция — это «как достать поле, по которому сравниваем». Мы как бы говорим алгоритму: «считай, что элемент — это его id».
#include <algorithm>
#include <ranges>
#include <vector>
std::vector<Task>::const_iterator findTaskById(
const std::vector<Task>& tasks, int id
) {
return std::ranges::find(tasks, id, &Task::id);
}
Читается почти как русский: «найди в tasks значение id, сравнивая по &Task::id».
6. Алгоритмы, которые пишут в выход, и составные результаты
Вот тут начинаются важные нюансы, из-за которых ranges‑алгоритмы сначала могут выглядеть «сложнее». Дело в том, что некоторые алгоритмы возвращают не один итератор, а «две позиции»: где закончился ввод, и где закончился вывод.
В классическом STL многие такие алгоритмы возвращали только итератор вывода (или вообще ничего), и полезная информация терялась. В ranges‑версии библиотека чаще возвращает составной результат.
std::ranges::copy и structured bindings
#include <algorithm>
#include <iterator>
#include <ranges>
#include <vector>
int main() {
std::vector<int> src{1, 2, 3};
std::vector<int> dst;
auto [in_pos, out_pos] = std::ranges::copy(src, std::back_inserter(dst));
(void)in_pos;
(void)out_pos;
}
Сама по себе распаковка через auto [a, b] — это тот самый случай, когда structured bindings внезапно спасают читаемость. Без них вам пришлось бы разбираться с типом результата (а он не обязан быть простым и красивым для новичка).
Практика для TodoLite: копируем выполненные задачи
Представим, что мы хотим получить список выполненных задач (например, чтобы распечатать отдельно или сохранить). Пока не обсуждаем «почему так делать архитектурно» — просто тренируем алгоритм.
#include <algorithm>
#include <iterator>
#include <ranges>
#include <vector>
std::vector<Task> collectDone(const std::vector<Task>& tasks) {
std::vector<Task> done;
auto [in_pos, out_pos] = std::ranges::copy_if(
tasks,
std::back_inserter(done),
[](const Task& t) { return t.done; }
);
(void)in_pos;
(void)out_pos;
return done;
}
Да, тут мы снова сделали (void)in_pos; — это просто способ «не ругаться на неиспользуемую переменную». В реальном коде эти позиции иногда полезны (например, для диагностики или частичного копирования), но на базовом уровне нам важнее сама идея: ranges‑алгоритмы могут возвращать больше информации.
7. Сортировка моделей короче: std::ranges::sort + проекция
Сортировка vector<Task> — классическая задача. И классический подход тоже классический: написать компаратор‑лямбду. Это нормально, но иногда хочется короче, особенно когда сортируем «по полю».
Сортировка с компаратором
#include <algorithm>
#include <vector>
void sortByPriority(std::vector<Task>& tasks) {
std::sort(tasks.begin(), tasks.end(),
[](const Task& a, const Task& b) {
return a.priority < b.priority;
});
}
То же самое через std::ranges::sort и проекцию
#include <algorithm>
#include <ranges>
#include <vector>
void sortByPriority(std::vector<Task>& tasks) {
std::ranges::sort(tasks, {}, &Task::priority);
}
Здесь {} — это «компаратор по умолчанию» (то есть обычное сравнение <), а &Task::priority — проекция: «сравниваем задачи по их priority».
Это один из тех моментов, когда ranges‑интерфейс делает код почти самодокументируемым. Правда, есть и обратная сторона: если вы пока не уверены, что такое «проекция», сначала это выглядит как заклинание из Хогвартса. Но хорошая новость в том, что это именно заклинание удобства, а не обязательная сложность.
8. Мини‑сборка: кусочек main и важная мысль про чтение кода
На этом этапе легко попасть в ловушку: «ranges — это новая парадигма, значит, всё надо переписать». Нет. В большинстве учебных и прикладных задач ranges‑алгоритмы — это те же алгоритмы, только с более дружелюбным интерфейсом.
Если вам нужно найти элемент, вы всё равно ищете. Если вам нужно отсортировать, вы всё равно сортируете. Сложность по времени у сортировки от смены синтаксиса не становится «магически быстрее» (увы), а вот читаемость часто становится лучше.
Полезная «самопроверка»: если вы можете вслух прочитать строку кода без внутренних рыданий, значит интерфейс работает как задумано. std::ranges::sort(tasks) читается легче, чем std::sort(tasks.begin(), tasks.end()), особенно когда выражение не короткое, а, скажем, getTasksFromUser() (но это уже отдельная история, и мы не будем сегодня трогать временные объекты).
Соберём маленький демонстрационный фрагмент, который показывает: мы можем хранить задачи, сортировать их и искать по id. Это не «финальное приложение», а просто точка, где видно, что примеры складываются.
#include <algorithm>
#include <iostream>
#include <ranges>
#include <vector>
int main() {
std::vector<Task> tasks{
{1, "Write report", false, 3},
{2, "Buy milk", true, 2},
{3, "Sleep", false, 1},
};
std::ranges::sort(tasks, {}, &Task::priority);
for (const Task& t : tasks) {
printTask(t);
}
// #3 [ ] Sleep (p=1)
// #2 [x] Buy milk (p=2)
// #1 [ ] Write report (p=3)
int id = 2;
auto it = std::ranges::find(tasks, id, &Task::id);
if (it != tasks.end()) {
std::cout << "Found: ";
printTask(*it); // Found: #2 [x] Buy milk (p=2)
}
}
Если вы поймали себя на мысли «о, это реально читается» — поздравляю, вы только что почувствовали главный смысл ranges‑интерфейса.
9. Типичные ошибки
Ошибка №1: путать std::ranges‑алгоритмы и «что-то про view’шки».
На этом месте мозг новичка часто делает неверный вывод: «ranges = это когда | и фильтры». На самом деле std::ranges — это большой мир, но в этой лекции мы говорили именно про алгоритмы (std::ranges::sort, find_if, copy_if). Они выполняют работу сразу. А «ленивые цепочки» и пайплайны — это уже отдельная история из соседней части ranges‑экосистемы.
Ошибка №2: забывать проверку результата поиска на end().
std::ranges::find_if возвращает итератор, и если элемент не найден, это будет «конец». Дальше новичок делает *it и получает приключения. Спасает дисциплина: «разыменовываю итератор только после if (it != v.end())».
Ошибка №3: пытаться выписать тип результата ranges‑алгоритма руками.
У std::ranges::copy и друзей возвращаемые типы выглядят серьёзно (и это нормально). В учебном коде почти всегда правильнее писать auto result = ...; или auto [a, b] = ...;. Это не «читерство», это современная норма: тип там нужен компилятору, а вам нужен смысл.
Ошибка №4: забывать нужные заголовки и потом винить «ranges как концепт».
Очень типичная ситуация: написали std::ranges::copy, но забыли <iterator> для std::back_inserter, или забыли <algorithm>, или <ranges>. В результате ошибка на полэкрана, и кажется, что сломалась вселенная. На самом деле сломался include. Когда видите странную ошибку про «не найдено имя» — первым делом проверяйте заголовки.
Ошибка №5: считать, что ranges‑алгоритмы «делают копию контейнера».
std::ranges::sort(tasks) сортирует именно tasks, то есть меняет контейнер на месте. Ranges‑интерфейс не означает «функциональный стиль без мутаций». Если алгоритм по смыслу меняет данные — он их меняет, и это надо помнить, чтобы не удивляться «почему порядок поменялся».
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ