Перейти к основному содержимому

Диапазоны и представления в C++20

Разработчику

Диапазоны (<ranges>, C++20) описывают последовательность элементов, с которой можно работать единообразно — контейнер, массив, ленивая цепочка преобразований. Связь с циклами: Циклы и управляющие конструкции в C++. Ограничения шаблонов: Работа с типами, Типы данных в C++.

Стандарт

Нужен C++20 (-std=c++20). В корпоративных проектах на C++17 этот раздел — ориентир на миграцию; см. Конфигурация и сборка.


Модель ranges

РольСмысл
Контейнергде лежат данные
viewкак на них смотреть (лениво)
алгоритмчто сделать

Конвейер читается слева направо: filtertransform → при необходимости материализация в контейнер.


Range и view

Range — всё, для чего есть begin/end (контейнер, C-массив, строка).

View — невладеющее представление: не копирует данные, вычисляет элементы по запросу (лениво). Владелец данных живёт в исходном контейнере; view не продлевает его жизнь.

#include <vector>
#include <ranges>

std::vector<int> data{10, 20, 30, 40, 50};
auto evens = data
| std::views::filter([](int x) { return x % 20 == 0; })
| std::views::transform([](int x) { return x / 10; });

for (int v : evens) { /* 1, 2, 3, 4, 5 */ }

Разбор:

  • Показан ленивый конвейер views::filter и views::transform, который обрабатывает данные без промежуточных контейнеров.
  • filter оставляет только подходящие значения, а transform применяет к ним вычисление перед выдачей в цикл.
  • Итоговые элементы вычисляются в момент прохода for, поэтому объявление evens само по себе не запускает расчёты.
  • Подход улучшает читаемость: этапы обработки описаны декларативно и в естественном порядке.
  • Такой стиль полезен, когда нужно собрать несколько шагов преобразования в один безопасный pipeline.

Пайп | читается слева направо: сначала фильтр, потом преобразование. Промежуточных vector нет.


Частые представления

ViewНазначение
filterОставить элементы по предикату
transformОтобразить каждый элемент
take / dropПервые N / пропустить N
take_while / drop_whileПока предикат истинен
reverseОбратный порядок
keys / valuesДля ассоциативных контейнеров (C++23: keys/values в std)

Пример "топ-3 после сортировки" без копирования всего контейнера в промежуточные векторы — сортировка всё же требует материализации; для уже отсортированных данных достаточно take.


Алгоритмы ranges

В <algorithm> есть перегрузки в пространстве std::ranges:

std::vector<int> v{3, 1, 4, 1, 5};
std::ranges::sort(v);
bool found = std::ranges::binary_search(v, 4);

Разбор:

  • std::ranges::sort(v) сортирует контейнер как единый диапазон, без передачи пары итераторов.
  • std::ranges::binary_search(v, 4) выполняет бинарный поиск уже по отсортированным данным.
  • Сначала идёт сортировка, затем поиск: это обязательный порядок для корректной работы бинарного поиска.
  • Ranges-API снижает риск ошибки с границами диапазона и делает вызовы короче.
  • Такой паттерн часто используют в задачах "подготовить данные -> быстро проверить наличие значения".

Итераторы передаются как один range-аргумент — меньше ошибок "перепутал begin/end".


Remove-erase через ranges (C++23)

Классика удаления по условию:

std::vector<int> v{1, 2, 3, 2, 4};
auto tail = std::ranges::remove(v, 2);
v.erase(tail.begin(), tail.end());

Разбор:

  • Здесь используется двухшаговая схема удаления: сначала remove, затем erase.
  • std::ranges::remove переставляет элементы и возвращает хвостовой диапазон, который больше не нужен.
  • erase удаляет этот хвост физически, уменьшая размер std::vector.
  • Логика совпадает с классическим remove-erase, но ranges-форма читается более цельно.
  • Такой подход безопаснее ручного удаления в цикле и проще для поддержки в команде.

В C++23std::erase / std::erase_if для контейнеров напрямую. Идиома в целом: Идиомы современного C++.


Проекции (C++23)

Проекция — "по какому полю сравнивать/сортировать":

struct User { std::string name; int age; };
std::vector<User> team{ /* ... */ };
std::ranges::sort(team, {}, &User::age); // по возрасту

Разбор:

  • Третий аргумент &User::age задаёт проекцию: сортировка сравнивает объекты по полю age.
  • Пустой компаратор {} оставляет стандартное сравнение, применяемое уже к результату проекции.
  • Код демонстрирует, как сортировать сложные структуры без отдельной лямбды-компаратора.
  • Проекции полезны для переиспользования: легко менять ключ сортировки, не переписывая алгоритм.
  • При работе с моделями данных это делает намерение кода очевидным на уровне сигнатуры вызова.

Третий аргумент — указатель на член или лямбда-проекция.


Опасности

  1. Висячие ссылки. View на временный объект:
auto bad = std::vector{1,2,3} | std::views::reverse; // UB после ;

Разбор:

  • Пример специально демонстрирует опасный сценарий со временем жизни данных.

  • View строится от временного std::vector, который уничтожается сразу после завершения выражения.

  • Переменная bad остаётся с висячей ссылкой на уже освобождённую память, это и есть источник UB.

  • Безопасный вариант: сначала сохранить контейнер в именованной переменной, затем строить view.

  • Этот фрагмент подчёркивает важное правило ranges: представления не владеют данными.

    Привязывайте view к именованному контейнеру с временем жизни длиннее использования.

  1. Инвалидация итераторов. filter/drop не меняют вектор, но erase при итерации по view — ошибка. Сначала материализуйте индексы или используйте erase-идиому на контейнере.

  2. Производительность. Ленивость выигрывает при короткой цепочке и одном проходе; многократный проход по тому же view с тяжёлым transform может быть хуже одного явного цикла.


Связь с концептами

Шаблоны вроде:

template<std::ranges::input_range R>
void dump(const R& r) {
for (const auto& x : r) { /* ... */ }
}

Разбор:

  • Шаблон ограничен концептом std::ranges::input_range, поэтому функция принимает только корректные диапазоны.
  • Это формирует явный контракт на уровне сигнатуры и упрощает сообщения компилятора при ошибках.
  • const R& позволяет передавать и контейнеры, и views без лишнего копирования.
  • Цикл работает единообразно для любых типов диапазонов, где доступны begin/end.
  • Такой подход масштабируется в библиотеках: API сразу фиксирует ожидаемые свойства аргумента.

принимают и vector, и ленивый view. Ошибки компиляции короче, чем у "голых" итераторов.


Когда оставить классический цикл

  • C++17 в продакшене без планов обновления;
  • критичный hot path, где профайлер показал регресс от абстракции;
  • сложная логика с несколькими выходами из цикла — явный for читается проще.

В остальном ranges делают код ближе к декларативному стилю и снижают число временных контейнеров.


Материализация результата: когда она обязательна

Views ленивые, но иногда нужен "настоящий" контейнер:

auto view = data
| std::views::filter([](int x) { return x > 0; })
| std::views::transform([](int x) { return x * 2; });

std::vector<int> out(view.begin(), view.end()); // материализация

Разбор:

  • В первой части формируется ленивый pipeline обработки данных через filter и transform.
  • Пока нет чтения элементов, вычислений тоже нет: создаётся описание преобразований.
  • Конструктор std::vector с парой итераторов материализует результат в владеющий контейнер.
  • После материализации out живёт независимо от исходного диапазона и безопасен для многократного прохода.
  • Это ключевой переход от "ленивого представления" к "стабильным данным для API и хранения".

Материализуйте, если:

  • результат нужен дольше жизни исходного диапазона;
  • по результату будет несколько проходов;
  • нужно передать данные в API, принимающий контейнер.

Частые ошибки чтения ranges-кода

Ошибка восприятияЧто на самом деле
"После filter у меня новый vector"нет, это view без владения
"Пайп всё вычислил сразу"нет, вычисление обычно при итерации
"Можно безопасно хранить view на временный объект"нельзя, будет висячая ссылка

Дальше