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

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

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

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

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


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 */ }

Пайп | читается слева направо: сначала фильтр, потом преобразование. Промежуточных 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);

Итераторы передаются как один 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());

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


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

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

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

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


Опасности

  1. Висячие ссылки. View на временный объект:

    auto bad = std::vector{1,2,3} | std::views::reverse; // UB после ;

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

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

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


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

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

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

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


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

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

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


Дальше

  • 30 — идиомы владения и копирования
  • 18 — алгоритмы STL
  • 1008 — задание на filter + transform

См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).