Диапазоны и представления в C++20
Диапазоны (<ranges>, C++20) описывают последовательность элементов, с которой можно работать единообразно — контейнер, массив, ленивая цепочка преобразований. Связь с циклами: Циклы и управляющие конструкции в C++. Ограничения шаблонов: Работа с типами, Типы данных в C++.
Нужен C++20 (-std=c++20). В корпоративных проектах на C++17 этот раздел — ориентир на миграцию; см. Конфигурация и сборка.
Модель ranges
| Роль | Смысл |
|---|---|
| Контейнер | где лежат данные |
| view | как на них смотреть (лениво) |
| алгоритм | что сделать |
Конвейер читается слева направо: filter → transform → при необходимости материализация в контейнер.
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++23 — std::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. - Пустой компаратор
{}оставляет стандартное сравнение, применяемое уже к результату проекции. - Код демонстрирует, как сортировать сложные структуры без отдельной лямбды-компаратора.
- Проекции полезны для переиспользования: легко менять ключ сортировки, не переписывая алгоритм.
- При работе с моделями данных это делает намерение кода очевидным на уровне сигнатуры вызова.
Третий аргумент — указатель на член или лямбда-проекция.
Опасности
- Висячие ссылки. View на временный объект:
auto bad = std::vector{1,2,3} | std::views::reverse; // UB после ;
Разбор:
-
Пример специально демонстрирует опасный сценарий со временем жизни данных.
-
View строится от временного
std::vector, который уничтожается сразу после завершения выражения. -
Переменная
badостаётся с висячей ссылкой на уже освобождённую память, это и есть источник UB. -
Безопасный вариант: сначала сохранить контейнер в именованной переменной, затем строить view.
-
Этот фрагмент подчёркивает важное правило ranges: представления не владеют данными.
Привязывайте view к именованному контейнеру с временем жизни длиннее использования.
-
Инвалидация итераторов.
filter/dropне меняют вектор, ноeraseпри итерации по view — ошибка. Сначала материализуйте индексы или используйте erase-идиому на контейнере. -
Производительность. Ленивость выигрывает при короткой цепочке и одном проходе; многократный проход по тому же 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 на временный объект" | нельзя, будет висячая ссылка |
Дальше
- Идиомы современного C++ — идиомы владения и копирования
- Стандартные и сторонние библиотеки C++ — алгоритмы STL
- Практические задания по C++ — задание на filter + transform