JavaRush /Курсы /C++ SELF /std::ranges‑алгоритмы

std::ranges‑алгоритмы

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

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‑алгоритм
Сортировка
std::sort(v.begin(), v.end())
std::ranges::sort(v)
Поиск по условию
std::find_if(v.begin(), v.end(), pred)
std::ranges::find_if(v, pred)
Подсчёт
std::count_if(v.begin(), v.end(), pred)
std::ranges::count_if(v, pred)
Копирование
std::copy(src.begin(), src.end(), out)
std::ranges::copy(src, out)

Обратите внимание: смысл не меняется. Меняется «форма вызова», и чаще всего — в сторону более читаемой.

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‑интерфейс не означает «функциональный стиль без мутаций». Если алгоритм по смыслу меняет данные — он их меняет, и это надо помнить, чтобы не удивляться «почему порядок поменялся».

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