библиотека — rangeslibrary — ranges

новые range-адаптерыnew range adaptors

views::zip, views::enumerate, views::chunk, views::slide, views::join_with, views::as_const/views::as_rvalue — самые частые "почему их не было" запросы к ranges из C++20, добавленные в C++23.views::zip, views::enumerate, views::chunk, views::slide, views::join_with, views::as_const/views::as_rvalue — the most common "why wasn't this in C++20 ranges" requests, added in C++23.

часто используетсяcommonly used частый вопрос: zip vs enumerate, chunk vs slidecommon question: zip vs enumerate, chunk vs slide

Что этоWhat it is

Эта страница — не введение в ranges (для основ, filter/transform/take/drop/reverse/iota и механики |, см. cpp20/ranges-basics.html), а список НОВЫХ view-адаптеров, которых не хватало в изначальном наборе C++20. views::zip(a, b, ...) объединяет несколько range поэлементно в range из tuple — по одному элементу из каждого источника на шаг. views::enumerate(v) даёт пары индекс+значение, аналог Python-овского enumerate. views::chunk(n) режет range на подряд идущие непересекающиеся куски по n элементов, а views::slide(n) — на скользящее окно размера n, где соседние окна перекрываются. views::join_with(delim) — аналог Python str.join, но для произвольных range из range: разворачивает вложенные последовательности в одну, вставляя разделитель между ними. views::as_const/views::as_rvalue — тонкие view-обёртки, форсирующие константность или rvalue-категорию элементов при обходе.This page isn't an introduction to ranges (for the basics — filter/transform/take/drop/reverse/iota and the mechanics of | — see cpp20/ranges-basics.html) — it's a list of the NEW view adaptors that were missing from C++20's original set. views::zip(a, b, ...) combines several ranges element-wise into a range of tuples — one element from each source per step. views::enumerate(v) gives index+value pairs, the analogue of Python's enumerate. views::chunk(n) slices a range into consecutive, non-overlapping pieces of n elements, while views::slide(n) gives a sliding window of size n, where adjacent windows overlap. views::join_with(delim) is the analogue of Python's str.join, but for arbitrary ranges of ranges: it flattens nested sequences into one, inserting a delimiter between them. views::as_const/views::as_rvalue are thin view wrappers that force element constness or rvalue category during iteration.

std::vector<std::string> names = {"Ann", "Bob", "Cid"};
std::vector<int> ages  = {30, 25, 41};

for (auto [name, age] : std::views::zip(names, ages)) {
    std::println("{} is {}", name, age);
}
// Ann is 30 / Bob is 25 / Cid is 41 — без ручного индексного цикла// Ann is 30 / Bob is 25 / Cid is 41 — no manual indexed loop

for (auto [i, name] : std::views::enumerate(names)) {
    std::println("{}: {}", i, name);   // 0: Ann / 1: Bob / 2: Cid// 0: Ann / 1: Bob / 2: Cid
}

Зачем ввелиWhy it was introduced

Изначальный набор views из C++20 покрывал основные декларативные операции, но оставлял пробелы в самых типичных задачах:C++20's original set of views covered the core declarative operations but left gaps in the most typical tasks:

01

"Пройтись по двум векторам одновременно" не имело декларативного решения"Walk two vectors at once" had no declarative solution

Без zip совместная итерация по двум и более range сводилась либо к индексному циклу (for (size_t i = 0; i < a.size(); ++i), с риском рассинхронизации границ), либо к std::transform с явным выходным итератором — оба варианта уводили от декларативного стиля, ради которого ranges вообще вводились.Without zip, iterating jointly over two or more ranges came down to either an indexed loop (for (size_t i = 0; i < a.size(); ++i), with a risk of mismatched bounds) or a std::transform with an explicit output iterator — both pulled away from the declarative style ranges were introduced for in the first place.

02

"Получить индекс вместе со значением" заставляло возвращаться к обычным циклам"Get the index along with the value" forced a fallback to plain loops

Без enumerate единственным способом получить индекс элемента внутри range-based for или цепочки views было вручную заводить и инкрементировать переменную-счётчик рядом с циклом — что снова возвращало код к до-ranges стилю и легко приводило к рассинхронизации счётчика с реальной позицией при использовании фильтрующих адаптеров выше по цепочке.Without enumerate, the only way to get an element's index inside a range-based for or a chain of views was to manually declare and increment a counter variable alongside the loop — which pulled the code back to a pre-ranges style and could easily desync the counter from the actual position when filtering adaptors sat earlier in the chain.

03

Разбиение на куски/окна писалось вручную под каждую задачуChunking/windowing was hand-rolled for every task

Задачи вроде "обработать данные пачками по 100 элементов" (chunk) или "посчитать скользящее среднее по трём соседним значениям" (slide) требовали либо написания собственного view-адаптера с нуля, либо ручных индексных циклов с арифметикой границ — то, ради избавления от чего изначально вводился ranges, оставалось не покрыто самим ranges.Tasks like "process data in batches of 100 elements" (chunk) or "compute a moving average over three neighboring values" (slide) required either writing a custom view adaptor from scratch or manual indexed loops with boundary arithmetic — the very thing ranges was introduced to eliminate remained uncovered by ranges itself.

Как это работает под капотомHow it works under the hood

Как и базовые views из C++20, все перечисленные адаптеры ленивы — не создают промежуточных контейнеров, а вычисляют очередной элемент результата при обращении к нему. chunk/slide над forward-и-выше range возвращают view из подпоследовательностей (каждая — сама по себе view над тем же хранилищем, без копирования элементов); zip прекращает итерацию, как только закончился САМЫЙ КОРОТКИЙ из входных range — аналогично поведению zip в Python, а не itertools.zip_longest.Like the basic C++20 views, all the adaptors listed here are lazy — they build no intermediate containers, computing the next result element only when it's accessed. chunk/slide over a forward-or-better range return a view of sub-sequences (each one itself a view over the same storage, with no element copying); zip stops iterating as soon as the SHORTEST of the input ranges runs out — matching Python's zip behavior, not itertools.zip_longest.

auto data = std::vector{1, 2, 3, 4, 5, 6, 7};

for (auto group : data | std::views::chunk(3)) {
    // group — view из 3 элементов, куски НЕ пересекаются:// group — a view of 3 elements, chunks do NOT overlap:
    // {1,2,3}, {4,5,6}, {7} — последний кусок короче, если не делится нацело// {1,2,3}, {4,5,6}, {7} — the last chunk is shorter if it doesn't divide evenly
}

for (auto window : data | std::views::slide(3)) {
    // window — view из 3 элементов, окна ПЕРЕСЕКАЮТСЯ, сдвиг на 1:// window — a view of 3 elements, windows DO overlap, shifting by 1:
    // {1,2,3}, {2,3,4}, {3,4,5}, {4,5,6}, {5,6,7}// {1,2,3}, {2,3,4}, {3,4,5}, {4,5,6}, {5,6,7}
}

std::vector<std::vector<int>> groups = {{1, 2}, {3, 4}, {5}};
for (int v : groups | std::views::join_with(0)) { /* 1 2 0 3 4 0 5 — разделитель между группами *//* 1 2 0 3 4 0 5 — a delimiter between groups */ }

views::enumerate отдаёт tuple-подобный элемент (пара индекс + ссылка на значение), который удобнее всего разбирать через structured bindings — for (auto [i, v] : views::enumerate(vec)) (см. cpp17/structured-bindings.html, если синтаксис auto [a, b] ещё не знаком). views::chunk/views::slide над input-only range (например, над однократно читаемым потоком) имеют более ограниченную семантику, чем над обычным контейнером с forward-и-выше итераторами — не каждая комбинация адаптеров одинаково хорошо работает с любой категорией итератора.views::enumerate yields a tuple-like element (an index paired with a reference to the value), which is most conveniently unpacked via structured bindings — for (auto [i, v] : views::enumerate(vec)) (see cpp17/structured-bindings.html if the auto [a, b] syntax isn't already familiar). views::chunk/views::slide over an input-only range (say, a stream readable only once) have more limited semantics than over an ordinary container with forward-or-better iterators — not every adaptor combination works equally well with every iterator category.

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

Параллельная итерация по ключам и значениям через zipParallel iteration over keys and values via zip

Два синхронных вектора (имена колонок и их значения) нужно вывести парами — без ручного индексного цикла и без риска обратиться за пределы более короткого вектора.Two synchronized vectors (column names and their values) need to be printed as pairs — with no manual indexed loop and no risk of reading past the shorter vector.

std::vector<std::string> columns = {"id", "name", "price"};
std::vector<std::string> values  = {"42", "widget", "9.99"};

for (auto [col, val] : std::views::zip(columns, values)) {
    std::println("{} = {}", col, val);
}
02

Нумерация строк лога через enumerateNumbering log lines via enumerate

Печать построчного вывода с индексом строки — типичная задача для утилит вроде cat -n, где индекс нужен только для отображения, не для логики.Printing line-by-line output with a line index — a typical task for tools like cat -n, where the index is needed only for display, not for logic.

std::vector<std::string> lines = {"first", "second", "third"};

for (auto [i, line] : std::views::enumerate(lines)) {
    std::println("{:>3}: {}", i + 1, line);
}
//   1: first /   2: second /   3: third//   1: first /   2: second /   3: third
03

Обработка данных пачками через chunkProcessing data in batches via chunk

Отправка элементов во внешнюю систему (сеть, БД) пачками фиксированного размера — типичная оптимизация для сокращения числа round-trip запросов.Sending elements to an external system (network, database) in fixed-size batches — a typical optimization to cut down the number of round-trip requests.

std::vector<int> ids(250);   // заполнены где-то выше// filled in above

for (auto batch : ids | std::views::chunk(100)) {
    send_batch_request(batch);   // 3 запроса: 100, 100, 50 элементов// 3 requests: 100, 100, 50 elements
}
04

Скользящее среднее через slideA moving average via slide

Сглаживание временного ряда — среднее по каждому окну из N соседних точек, классическая задача для skользящего окна вместо chunk, где окна должны пересекаться.Smoothing a time series — the average over each window of N neighboring points, a classic task for a sliding window rather than chunk, where windows must overlap.

std::vector<double> prices = {10.0, 10.5, 11.0, 10.8, 11.2};

for (auto window : prices | std::views::slide(3)) {
    double avg = std::ranges::fold_left(window, 0.0, std::plus{}) / 3.0;
    std::println("{:.2f}", avg);   // среднее по каждым 3 соседним ценам// the average over each 3 neighboring prices
}

ГраблиGotchas

zip останавливается по самому короткому range, а не выравнивает длиныzip stops at the shortest range instead of padding lengths

views::zip(a, b), если a и b разной длины, отдаст ровно min(a.size(), b.size()) пар и молча остановится — как zip в Python, а не как itertools.zip_longest с заполнением недостающих мест. Если ranges разной длины ожидаемы, а не признак ошибки где-то раньше в конвейере, эту семантику нужно продумать явно — стандартная библиотека не выбрасывает исключение и не предупреждает об усечении.views::zip(a, b), when a and b differ in length, yields exactly min(a.size(), b.size()) pairs and stops silently — like Python's zip, not like itertools.zip_longest padding the missing slots. If ranges of differing length are expected rather than a sign of a bug earlier in the pipeline, this semantics needs to be accounted for explicitly — the standard library neither throws nor warns about the truncation.

enumerate отдаёт tuple-подобный тип, разбираемый через structured bindingsenumerate yields a tuple-like type, unpacked via structured bindings

Элемент views::enumerate(v) — не std::pair<size_t, T> буквально, а специфичный для реализации tuple-like тип (std::ranges::range_value_t данного view), который просто ведёт себя как пара при structured bindings — for (auto [i, v] : views::enumerate(vec)) (см. cpp17/structured-bindings.html). Попытка сохранить этот тип в явно написанной переменной без auto или разобрать его как обычный std::pair через .first/.second может не скомпилироваться в зависимости от реализации.The element of views::enumerate(v) is not literally a std::pair<size_t, T>, but an implementation-specific tuple-like type (the view's own std::ranges::range_value_t) that simply behaves like a pair under structured bindings — for (auto [i, v] : views::enumerate(vec)) (see cpp17/structured-bindings.html). Trying to store that type in an explicitly-typed variable without auto, or to unpack it as a plain std::pair via .first/.second, may not compile depending on the implementation.

chunk/slide над input-только range работают более ограниченноchunk/slide over an input-only range work more restrictively

Над обычным контейнером (forward-и-выше итераторы) chunk/slide дают полноценные, многократно проходимые view. Над input-only range (например, однократно читаемым потоком, который нельзя перемотать назад) поведение и набор доступных операций у этих адаптеров ограниченнее — не каждая комбинация view одинаково хорошо работает с любой категорией итератора, это стоит проверять для конкретного источника данных, а не считать универсальным по умолчанию.Over an ordinary container (forward-or-better iterators), chunk/slide give full, multi-pass views. Over an input-only range (say, a stream readable only once, with no way to rewind), the behavior and the set of available operations on these adaptors are more limited — not every view combination works equally well with every iterator category, and this is worth checking for a specific data source rather than assumed to be universal by default.