Привет, Хабр!
С выходом C++20 библиотека Range получила свое официальное место в языке, что ознаменовало некоторый важный шаг в развитии работы с контейнерами и итераторами. Это обновление ввело новый подход к манипуляциям с данными.
Итак, что же делает Range таким особенным? Традиционные итераторы требуют большого объема кода для выполнения простых операций вроде фильтрации или сортировки данных. С Range можно избавиться от этой сложности, с помощью интуитивно понятному и лаконичному способу работы с коллекциями данных. В этой статье мы и рассмотрим основные концепции библиотеки Range.
Основные концепции
Диапазоны — это основа библиотеки Range. Они представляют собой контейнеры или другие структуры данных, которые могут быть перебираемы. Основная идея состоит в том, чтобы описывать манипуляции с данными как последовательность преобразований.
Пример работы с диапазонами:
#include <iostream> #include <ranges> #include <vector> int main() { std::vector<int> numbers = {1, 2, 3, 4, 5, 6}; auto even_numbers = numbers | std::views::filter([](int n) { return n % 2 == 0; }); for (int n : even_numbers) { std::cout << n << " "; // Вывод: 2 4 6 } }
Используем функцию std::views::filter
, чтобы получить только четные числа из исходного диапазона. Диапазоны выглядят более выразительно по сравнению с традиционными итераторами.
Диапазоны не являются просто одним из способов итерирования контейнеров, а представляют собой полноценную абстракцию для работы с данными. Стандартные итераторы требуют постоянного поддержания некой точки доступа к данным, тогда как диапазоны работают на более высоком уровне — они «абстрагируются» от реальных данных и позволяют работать с последовательностями независимо от их реализации (будь то массивы, вектора, списки и т.д.).
Одно из сильных преимуществ диапазонов — это возможность линейно комбинировать множество преобразований без создания промежуточных контейнеров. Если бы мы применяли подобные операции без диапазонов, нам пришлось бы сохранять результаты каждого шага в новом контейнере. Диапазоны решают эту проблему благодаря ленивой обработке.
Views — это особый тип диапазонов, которые не копируют данные, а создают ленивые вычисления. Представления действуют как фильтры или трансформаторы данных: они «видят» исходные данные, но не изменяют их, а создают новую последовательность на основе исходной коллекции.
Особенность представлений — это ленивость. То есть, данные не обрабатываются сразу, а только тогда, когда они действительно необходимы (например, при итерации).
Пример использования представлений:
#include <iostream> #include <ranges> #include <vector> int main() { std::vector<int> numbers = {1, 2, 3, 4, 5, 6}; auto square_even_numbers = numbers | std::views::filter([](int n) { return n % 2 == 0; }) | std::views::transform([](int n) { return n * n; }); for (int n : square_even_numbers) { std::cout << n << " "; // Вывод: 4 16 36 } }
Здесь сначала фильтруются четные числа, а затем каждое из них возводится в квадрат с помощью std::views::transform
. Поскольку представления ленивы, оба преобразования применяются только тогда, когда начинается итерация по результату.
Адаптеры — это функции, которые преобразуют диапазоны. Адаптеры применяются к диапазонам с помощью оператора |
.
Наиболее часто используемые адаптеры:
-
std::views::filter
— фильтрует элементы на основе условия. -
std::views::transform
— применяет функцию к каждому элементу диапазона. -
std::views::take
— берёт первые N элементов диапазона. -
std::views::drop
— пропускает первые N элементов диапазона.
Пример с адаптерами:
#include <iostream> #include <ranges> #include <vector> int main() { std::vector<int> numbers = {1, 2, 3, 4, 5, 6}; auto result = numbers | std::views::filter([](int n) { return n % 2 == 0; }) | std::views::take(2) | std::views::transform([](int n) { return n * 2; }); for (int n : result) { std::cout << n << " "; // Вывод: 4 8 } }
Комбинируем несколько адаптеров: сначала фильтруем четные числа, затем берём только первые два элемента и, наконец, удваиваем их. Всё это осуществляется в ленивой манере, без копирования данных.
Можно комбинировать диапазоны, представления и адаптеры, тем самым создавая цепочки преобразований данных, минимизируя сложность кода. Все эти преобразования будут проходить лениво — данные не обрабатываются до тех пор, пока не начнётся фактическая итерация.
Например:
auto result = std::views::iota(1, 100) // создаём диапазон от 1 до 100 | std::views::filter([](int n) { return n % 2 == 0; }) // фильтруем только чётные | std::views::transform([](int n) { return n * n; }) // возводим в квадрат | std::views::take(10); // берём первые 10 элементов for (int n : result) { std::cout << n << " "; // Вывод: 4 16 36 64 100 144 196 256 324 400 }
Диапазоны с ленивыми вычислениями позволяют работать с большими наборами данных, не загружая память.
Range с контейнерами STL
std::vector
— это наиболее распространённый контейнер в C++, и его можно использовать с библиотекой Range для выполнения фильтрации, сортировки и трансформаций данных.
Пример фильтрации четных чисел и возведение их в квадрат:
#include <iostream> #include <ranges> #include <vector> int main() { std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; auto even_squares = numbers | std::views::filter([](int n) { return n % 2 == 0; }) | std::views::transform([](int n) { return n * n; }); for (int n : even_squares) { std::cout << n << " "; // Вывод: 4 16 36 64 100 } }
std::views::filter
фильтрует четные числа, а std::views::transform
возводит их в квадрат. Оба этих процесса происходят лениво, и данные не преобразуются, пока мы не начнём итерировать по результату.
std::list
отличается от std::vector
тем, что предоставляет двусвязный список, который поддерживает вставки и удаления в произвольных местах без необходимости смещения всех последующих элементов.
Пример извлечения и возведение в квадрат элементов списка с условием:
#include <iostream> #include <ranges> #include <list> int main() { std::list<int> numbers = {10, 15, 20, 25, 30}; auto transformed = numbers | std::views::filter([](int n) { return n % 5 == 0; }) | std::views::transform([](int n) { return n * n; }); for (int n : transformed) { std::cout << n << " "; // Вывод: 25 100 225 400 900 } }
Диапазоны могут работать с std::list
, лениво преобразовывая и фильтруя его содержимое.
std::forward_list
— это односвязный список, который поддерживает только последовательный доступ, и работа с ним через итераторы может быть несколько ограниченной. Однако благодаря библиотеке Range можно немного упростить этот процесс.
Например, пропустим первые два элемента и возьмем следующие три:
#include <iostream> #include <ranges> #include <forward_list> int main() { std::forward_list<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8}; auto result = numbers | std::views::drop(2) // пропускаем первые два элемента | std::views::take(3); // берем следующие три for (int n : result) { std::cout << n << " "; // Вывод: 3 4 5 } }
Даже с таким простым контейнером, как std::forward_list
, Range позволяет управлять данными, используя такие адаптеры, как std::views::drop
и std::views::take
.
Пользовательские диапазоны и адаптера
Создание пользовательских диапазонов основывается на концепции итераторов и диапазонов в C++. Для этого достаточно реализовать необходимые методы begin()
и end()
.
Пример простого пользовательского диапазона, который генерирует последовательность чисел:
#include <iostream> #include <ranges> class CustomRange { public: CustomRange(int start, int end) : current(start), end_value(end) {} auto begin() const { return current; } auto end() const { return end_value; } private: int current; int end_value; }; int main() { CustomRange range(1, 10); for (int n : range) { std::cout << n << " "; // Вывод: 1 2 3 4 5 6 7 8 9 } }
Создали простой диапазон, который можно использовать в цикле for
.
А вот уже создание пользовательского адаптера требует реализации функции, которая возвращает новый диапазон или изменённый вид существующего диапазона.
Пример создания пользовательского адаптера:
#include <iostream> #include <ranges> #include <vector> struct custom_transform { int multiplier; custom_transform(int m) : multiplier(m) {} auto operator()(int n) const { return n * multiplier; } }; int main() { std::vector<int> numbers = {1, 2, 3, 4, 5}; auto transformed = numbers | std::views::transform(custom_transform(3)); // Умножаем все элементы на 3 for (int n : transformed) { std::cout << n << " "; // Вывод: 3 6 9 12 15 } }
Создаем кастомный адаптер, который умножает каждый элемент на заданное число.
Подробнее с Range можно ознакомиться здесь.
А на бесплатном вебинаре специализации C++ Developer коллеги из OTUS расскажут из каких этапов состоит компиляция программы на С++, покажут результаты выполнения каждого этапа, и проговорят возможные проблемы и их решения. Регистрация доступна по ссылке.
ссылка на оригинал статьи https://habr.com/ru/articles/841456/
Добавить комментарий