Последнее обновление:2026-10-12
Библиотека Ranges в C++20¶
ranges — одна из главных возможностей C++20, и "view" является весьма важной её частью. До C++20 алгоритмы стандартной библиотеки реализовывались на основе итераторов, например: std::sort.
Итераторы + алгоритмы позволяют выполнять ряд сложных операций; например, я хочу отсортировать в обратном порядке:
Но это сопряжено с некоторыми проблемами:
- Легко смешать два несовместимых итератора.
- Композиционность алгоритмов слишком слабая, приходится хранить промежуточные переменные
Например: пусть есть система информации о студентах, и мы хотим вычислить суммарный GPA студентов в возрасте от 21 до 25 лет с GPA >= 3.5. До C++17 мы могли бы написать такой псевдокод:
std::vector<Student> students
std::vector<Student> selected;
std::copy_if(students.begin(), students.end(), std::back_inserter(selected),
[](const Student& student) {
return student.age >= 21 && student.age <= 25 && student.gpa >= 3.5;
});
double s = std::accumulate(
selected.begin(), selected.end(), 0.0,
[](double sum, const Student& student) { return sum + student.gpa; });
Видно, что при фильтрации студентов, удовлетворяющих условиям, нам приходится копировать их в selected и лишь затем суммировать.
C++20 привнёс более лаконичный и эффективный способ: используя диапазоны и оператор конвейера | для соединения нескольких операций, можно просуммировать напрямую, без промежуточных переменных, например:
double s = 0.0;
for (const auto& student :
students | std::views::filter([](const auto& s) {
return s.age >= 21 && s.age <= 25 && s.gpa >= 3.5;
}) | std::views::transform([](const auto& s) { return s.gpa; })) {
s += student;
}
Видно, что несколько алгоритмов стыкуются бесшовно, уменьшая выделение памяти под промежуточные временные переменные, а читабельность значительно возрастает: конвейер | соединяет разные операции воедино.
Давайте вместе разберёмся с материалом, связанным с ranges в C++20.
1.Базовые понятия¶
1.range
range — это абстрактное понятие, обозначающее последовательность. Это может быть любой контейнер с итераторами или объект, определяющий функции begin() и end(). Например, std::vector, std::list и другие — примеры диапазонов; массив тоже можно рассматривать как диапазон.
2.view
view — это доступ только для чтения к Range. Это способ ленивых вычислений: расчёт происходит только тогда, когда он нужен, а это значит, что view фактически не хранит данные. Например, std::views::filter и std::views::transform — типичные примеры view. Они позволяют фильтровать и преобразовывать range, не создавая фактически новый контейнер.
3.algorithm
Алгоритмы — это функции, работающие с range или view; например, std::sort, std::find и другие — примеры алгоритмов.
4.|
Оператор конвейера | может соединять представления и алгоритмы, передавая результат слева на вход справа. Он делает код более ясным и лаконичным. Например: students | std::views::filter(...) передаёт диапазон students в std::views::filter для фильтрации, а затем результат передаётся последующим операциям.
Возьмём для примера описанное выше вычисление по студентам: здесь мы используем диапазон students как вход представления filter через |, затем результат подаём на вход представления transform, и в конце возвращается диапазон, по которому мы итерируемся в цикле, получая сумму с помощью алгоритма накопления.
double s = 0.0;
for (const auto& student :
students | std::views::filter([](const auto& s) {
return s.age >= 21 && s.age <= 25 && s.gpa >= 3.5;
}) | std::views::transform([](const auto& s) { return s.gpa; })) {
s += student;
}
Видно, что использование range даёт такие преимущества:
- Повышение читабельности кода
- Ленивые вычисления: Ranges вводят понятие представления (view), позволяя ленивые вычисления, то есть вычисление результата только тогда, когда он нужен.
Например: вычисление произойдёт только при обращении *v.begin().
- Меньше ошибок: дизайн Ranges помогает избежать ряда ошибок, типичных для классических операций с итераторами, например использования несовместимых итераторов.
- и так далее.
Концепция диапазона вводит разные понятия для описания разных типов диапазонов. Эти понятия помогают в обобщённом программировании лучше понимать и ограничивать свойства диапазонов. Вот некоторые часто используемые понятия диапазонов:
| Понятие | Описание | Примеры контейнеров |
|---|---|---|
std::ranges::input_range |
Можно итерировать от начала до конца хотя бы раз | std::forward_list、std::list、std::duque、std::array、std::vector |
std::ranges::forward_range |
Можно итерировать от начала до конца много раз | std::forward_list、std::list、std::duque、std::array、std::vector |
std::ranges::bidirectional_range |
Итератор может также двигаться назад -- |
std::list、std::duque、std::array、std::vector |
std::ranges::random_access_range |
Можно перейти к элементу [] за константное время |
std::duque、std::array、std::vector |
std::ranges::contiguous_range |
Элементы всегда хранятся в памяти непрерывно | std::array、std::vector |
2.Использование¶
Использовать эту возможность довольно просто: достаточно подключить заголовочный файл и использовать интерфейсы.
Например: отфильтровать чётные числа из набора чисел.
#include <ranges>
auto evenNumbers = numbers | std::views::filter([](int x) { return x % 2 == 0; });
Компиляция: достаточно указать -std.
О поддержке компиляторами можно прочитать в списке ниже:
gcc >=10, clang >=13 (частично), >=15 — полная поддержка.
Похожие темы¶
- Введение в корутины C++20 | C++那些事
- Практика с channel в C++20 | C++那些事
- Концепции Concepts в C++20 | C++那些事
- constexpr и оператор «запятая» в C++20 | C++那些事
- Порядок байтов endian в C++20 | C++那些事
- Лямбда-выражения в C++20 (часть 1) | C++那些事
- Модули Modules в C++20 | C++那些事
- Дизайн и подводные камни std::span в C++20 | C++那些事
- Обзор новых возможностей C++20 | C++那些事
- [[likely]] и [[unlikely]] в C++20 | C++那些事