библиотека — rangeslibrary — ranges
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.
Эта страница — не введение в 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
}
Изначальный набор views из C++20 покрывал основные декларативные операции, но оставлял пробелы в самых типичных задачах:C++20's original set of views covered the core declarative operations but left gaps in the most typical tasks:
Без 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.
Без 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.
Задачи вроде "обработать данные пачками по 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.
Как и базовые 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.
Два синхронных вектора (имена колонок и их значения) нужно вывести парами — без ручного индексного цикла и без риска обратиться за пределы более короткого вектора.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);
}
Печать построчного вывода с индексом строки — типичная задача для утилит вроде 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
Отправка элементов во внешнюю систему (сеть, БД) пачками фиксированного размера — типичная оптимизация для сокращения числа 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
}
Сглаживание временного ряда — среднее по каждому окну из 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
}
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.
Элемент 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.
Над обычным контейнером (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.