Понятие контейнер сейчас активно применяется в контексте Docker и аналогичных решений по контейнеризации. Однако, в языке C++ контейнеры существуют уже очень давно и являются фундаментальной частью Standard Template Library (STL). Они предоставляют готовые реализации наиболее часто используемых структур данных, избавляя разработчика от необходимости писать их с нуля.
Контейнеры C++ можно разделить на несколько основных категорий. Последовательные контейнеры (Sequence Containers) хранят элементы в линейной последовательности, при этом порядок элементов определяется позицией добавления. Ассоциативные контейнеры (Associative Containers) автоматически сортируют элементы по ключу. При этом, они обеспечивают достаточно быстрый поиск (O(log n)). Неупорядоченные ассоциативные контейнеры (Unordered Associative Containers) хранят элементы в хеш-таблицах. Обеспечивают поиск в среднем за O(1). Адаптеры контейнеров (Container Adapters) предоставляют ограниченный интерфейс поверх других контейнеров.
В рамках данной статьи мы будем говорить об использовании библиотеки Ranges для работы с контейнерами.
Почему Ranges
До появления библиотеки Ranges стандартный подход к обработке данных в контейнерах заключался в передаче алгоритмам пары итераторов begin() и end(). Например, сортировка вектора выглядела так: std::sort(v.begin(), v.end());.
Такой подход был вполне работоспособен, но имел свои недостатки. При построении сложной цепочки преобразований (например, "отфильтровать четные числа, возвести их в квадрат и взять первые 5") код становился многословным и требовал создания промежуточных контейнеров для хранения результатов каждого шага.
Библиотека Ranges вводит понятие диапазона (range). Диапазон — это сущность, которую можно итерировать. Он определяется итератором на начало и специальным маркером (sentinel) на конец, который может быть, как итератором, так и отдельным типом. Благодаря этому новому уровню абстракции, алгоритмы из пространства имен std::ranges могут напрямую принимать контейнеры целиком: std::ranges::sort(v);.
Давайте посмотрим подробнее, что из себя представляет данная библиотека. Ее сердце составляют две взаимосвязанные концепции.
Представление (View) — это легковесный диапазон, который не владеет данными. Его семантика копирования и перемещения должна быть очень быстрой (константное время). Представления создаются путем применения преобразований к другим диапазонам, но сами эти преобразования вычисляются “лениво”. Это означает, что элементы обрабатываются не в момент создания представления, а только когда мы начинаем их итерировать. Такой подход позволяет избежать выделения дополнительной памяти и лишних вычислений.
Адаптер диапазона (Range Adaptor) представляет собой объект, который принимает на вход один или несколько диапазонов (или другие параметры) и возвращает новое представление. Адаптеры можно объединять в конвейеры (pipelines) с помощью оператора | (pipe), что делает код невероятно читаемым.
Например, запись v | std::views::filter(pred) | std::views::transform(f) создает представление, которое фильтрует исходный вектор v с помощью предиката pred, а затем применяет функцию f к каждому оставшемуся элементу. При этом ни фильтрация, ни преобразование реально не происходят, пока мы не начнем обход результата, например, в цикле for.
Далее давайте рассмотрим несколько примеров, демонстрирующих различные возможности Ranges.
Начнем с отсеивания и преобразования элементов. Допустим, у нас есть вектор чисел, и нам нужно получить квадраты только четных чисел. Классический подход требует промежуточного вектора:
std::vector<int> input = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; std::vector<int> intermediate, output; std::copy_if(input.begin(), input.end(), std::back_inserter(intermediate), [](int i) { return i % 2 == 0; }); std::transform(intermediate.begin(), intermediate.end(), std::back_inserter(output), [](int i) { return i * i; });
Решение с Ranges не только короче, но и эффективнее, так как не создает промежуточный вектор intermediate:
#include <ranges> #include <vector> #include <iostream> int main() { std::vector<int> input = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; auto result_view = input | std::views::filter([](int n) { return n % 2 == 0; }) | std::views::transform([](int n) { return n * n; }); for (int val : result_view) { std::cout << val << ' '; // Вывод: 0 4 16 36 64 100 } std::cout << '\n'; return 0; }
Здесь полученные на вход числа проходят фильтрацию на предмет четности std::views::filter, после чего std::views::transform выполняет необходимые вычисления.
Далее посмотрим композицию нескольких операций. Ranges позволяют легко комбинировать множество операций в одном пайплайне, сохраняя код чистым и понятным. В качестве примера рассмотрим следующую задачу: взять числа от 1 до бесконечности, выбрать нечетные, возвести их в квадрат, взять первые 5 и вывести в обратном порядке.
#include <ranges> #include <iostream> int main() { auto result = std::views::iota(1) // Бесконечная последовательность 1, 2, 3, ... | std::views::filter([](int n) { return n % 2 == 1; }) // Оставляем нечетные | std::views::transform([](int n) { return n * n; }) // Возводим в квадрат | std::views::take(5) // Берем первые 5: 1, 9, 25, 49, 81 | std::views::reverse; // Разворачиваем: 81, 49, 25, 9, 1 for (int val : result) { std::cout << val << ' '; } // Вывод: 81 49 25 9 1 return 0; }
Как видно, с помощью такого пайплайна можно достаточно легко и наглядно выполнить вычисления. В этом примере std::views::iota — это фабрика диапазонов, генерирующая бесконечную последовательность. Благодаря “ленивости” вычислений, программа не пытается создать бесконечный массив, а генерирует и обрабатывает числа по мере необходимости в цикле for.
Работа с парами "ключ-значение"
Для ассоциативных контейнеров, таких как std::map, в Ranges есть специальные адаптеры std::views::keys и std::views::values, которые позволяют работать отдельно с ключами или значениями. Это делает работу с кодом более удобной и простой.
#include <ranges> #include <map> #include <iostream> int main() { std::map<std::string, int> ages = {{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}}; // Получить все ключи (имена) auto names = ages | std::views::keys; for (const auto& name : names) { std::cout << name << ' '; // Вывод: Alice Bob Charlie (порядок может отличаться) } std::cout << '\n'; // Получить все значения (возраст) и отфильтровать их auto adult_ages = ages | std::views::values | std::views::filter([](int age) { return age >= 30; }); for (int age : adult_ages) { std::cout << age << ' '; // Вывод: 30 35 } std::cout << '\n'; return 0; }
Конструирование контейнеров из диапазонов
Одним из главных неудобств при работе с Ranges в C++ был финальный шаг: преобразование полученного представления обратно в конкретный контейнер, например, в std::vector. Представление — это легковесная "проекция" данных, и ее нельзя просто присвоить вектору. Приходилось использовать громоздкий конструктор, принимающий итераторы begin() и end() от представления.
После выхода C++ версии 23 полностью решает эту проблему, вводя новую перегрузку конструкторов для всех стандартных контейнеров, которая принимает специальный тег std::from_range_t и сам диапазон. Это позволяет создавать контейнеры непосредственно из любого диапазона.
Вот пример создания вектора из нашего первого пайплайна с четными квадратами в C++ версии 23:
#include <ranges> #include <vector> #include <iostream> int main() { std::vector<int> input = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; auto result_view = input | std::views::filter([](int n) { return n % 2 == 0; }) | std::views::transform([](int n) { return n * n; }); // C++23: Прямое конструирование вектора из диапазона std::vector<int> output(std::from_range, result_view); for (int val : output) { std::cout << val << ' '; // Вывод: 0 4 16 36 64 100 } std::cout << '\n'; return 0; }
Как видно, теперь наш пайплайн становится полностью замкнутым: от контейнера через серию преобразований к новому контейнеру. Это делает код еще более цельным и избавляет от ручного управления итераторами.
Заключение
Библиотека Ranges существенно упрощает написание алгоритмов на C++. Она органично вписывается в STL, расширяя его принципами функционального программирования. В C++ версии 20 был заложен фундамент с ленивыми представлениями и композицией через оператор |. Версия 23 сделала библиотеку еще более практичной, добавив возможность прямого конструирования контейнеров из диапазонов. Освоив Ranges, вы сможете писать более лаконичный, выразительный и эффективный код, который легче читать, тестировать и сопровождать. Это не просто новый синтаксис, а более мощный и абстрактный способ мышления об обработке данных.

Если хочется разобраться в C++ системно, а не собирать язык по кускам из STL, шаблонов и случайных примеров, в Отус есть специализация «Разработчик на C++». Там с нуля последовательно проходят базу и практику разработки, чтобы современные возможности языка становились рабочим инструментом, а не набором разрозненных приёмов.
Для знакомства с форматом обучения и экспертами приходите на бесплатные уроки:
17 марта в 20:00. «Выравнивание данных в C++: как память влияет на скорость и эффективность программ». Записаться
24 марта в 20:00. «Алгоритмы и структуры данных на С++». Записаться
Полный список бесплатных уроков марта смотрите в дайджесте.
