C++20 Ranges: новая парадигма
Откройте для себя C++20 Ranges — современную переработку интерфейса алгоритмов STL. Изучите концепты диапазонов, алгоритмы на основе диапазонов, проекции и оператор pipe для компонуемых преобразований данных.
Зачем нужны Ranges?
C++20 Ranges — это фундаментальное обновление модели алгоритмов STL. Традиционная STL требует передачи пар итераторов каждому алгоритму, что громоздко и подвержено ошибкам (можно случайно передать итераторы от разных контейнеров). Ranges решают эту проблему, позволяя передавать сам контейнер — или любой объект, определяющий begin() и end().
Ключевые улучшения по сравнению с традиционными алгоритмами:
1. Передача контейнеров напрямую — std::ranges::sort(vec) вместо std::sort(vec.begin(), vec.end())
2. Проекции — преобразование элементов перед сравнением без их модификации
3. Концепты — алгоритмы имеют чёткие требования времени компиляции
4. Компонуемость — цепочка операций через оператор pipe |
5. Ленивое вычисление — представления вычисляют элементы по запросу, избегая промежуточных копий
Концепты диапазонов
C++20 определяет иерархию концептов диапазонов в :
- std::ranges::range — имеет begin() и end()
- std::ranges::sized_range — имеет size() за O(1)
- std::ranges::input_range — может быть прочитан однократно (как итераторы ввода)
- std::ranges::forward_range — многопроходный прямой обход
- std::ranges::bidirectional_range — прямой + обратный
- std::ranges::random_access_range — двунаправленный + O(1) прыжки
- std::ranges::contiguous_range — произвольный доступ + непрерывная память
- std::ranges::common_range — begin() и end() возвращают одинаковый тип
- std::ranges::viewable_range — может быть преобразован в представление
Эти концепты используются для ограничения алгоритмов на основе диапазонов, обеспечивая понятные сообщения об ошибках при передаче несовместимого диапазона.
Алгоритмы на основе диапазонов
Алгоритмы на основе диапазонов находятся в std::ranges:: и принимают целые контейнеры. Они также возвращают более богатые типы результатов вместо голых итераторов.
#include <algorithm>
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
struct Employee {
std::string name;
int age;
double salary;
};
int main() {
std::vector<int> nums = {5, 3, 1, 4, 1, 5, 9, 2, 6};
// Pass the container directly — no begin/end needed
std::ranges::sort(nums);
// nums: {1, 1, 2, 3, 4, 5, 5, 6, 9}
// find returns a borrowed iterator (safe with temporary ranges)
auto it = std::ranges::find(nums, 4);
if (it != nums.end()) {
std::cout << "Found: " << *it << '\n';
}
// count_if with ranges
auto even_count = std::ranges::count_if(nums, [](int x) { return x % 2 == 0; });
std::cout << "Even count: " << even_count << '\n'; // 3
// min/max on entire range
auto [min_it, max_it] = std::ranges::minmax_element(nums);
std::cout << "Min: " << *min_it << ", Max: " << *max_it << '\n';
// Sorting employees by salary (descending)
std::vector<Employee> team = {
{"Alice", 30, 95000},
{"Bob", 25, 72000},
{"Charlie", 35, 110000},
};
std::ranges::sort(team, std::ranges::greater{}, &Employee::salary);
// Charlie (110k), Alice (95k), Bob (72k)
for (const auto& e : team) {
std::cout << e.name << ": $" << e.salary << '\n';
}
}Проекции — преобразование перед сравнением
Проекции — уникальная особенность алгоритмов на основе диапазонов. Они позволяют указать, как «видеть» каждый элемент для сравнения или обработки, не модифицируя сам элемент.
#include <algorithm>
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
#include <cctype>
int main() {
// Sort strings case-insensitively using a projection
std::vector<std::string> words = {"Banana", "apple", "Cherry", "date"};
std::ranges::sort(words, {}, [](const std::string& s) {
std::string lower = s;
std::transform(lower.begin(), lower.end(), lower.begin(),
[](char c) { return std::tolower(c); });
return lower;
});
// words: {"apple", "Banana", "Cherry", "date"}
// Original strings preserved, sorted by lowercase version
for (const auto& w : words) std::cout << w << ' ';
std::cout << '\n';
// Using member pointer as projection
struct Point { int x, y; };
std::vector<Point> points = {{3, 1}, {1, 5}, {2, 3}};
// Sort by x coordinate
std::ranges::sort(points, {}, &Point::x);
// points: {{1,5}, {2,3}, {3,1}}
// Find max by y coordinate
auto max_y = std::ranges::max_element(points, {}, &Point::y);
std::cout << "Max y: (" << max_y->x << ", " << max_y->y << ")\n"; // (1, 5)
// Binary search with projection
bool found = std::ranges::binary_search(points, 2, {}, &Point::x);
std::cout << std::boolalpha << "Has x=2: " << found << '\n'; // true
}Оператор pipe для композиции
Оператор pipe | — визитная карточка C++20 Ranges. Он позволяет связывать адаптеры диапазонов (представления) в конвейер, который читается слева направо, подобно pipe в командной оболочке Unix:
auto result = numbers
| std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; })
| std::views::take(5);Читается естественно: «берём числа, оставляем чётные, возводим в квадрат, берём первые 5». Конвейер является ленивым — никакие вычисления не происходят, пока вы не начнёте итерировать result. Это означает, что промежуточные векторы не создаются.
Мы подробно рассмотрим представления в следующем уроке.
Алгоритмы диапазонов защищают от висячих итераторов из временных диапазонов. Если вы передадите временный контейнер, возвращённый итератор будет висячим, потому что временный объект уничтожается в конце выражения:
auto it = std::ranges::find(get_vector(), 42);
// Возвращает std::ranges::dangling вместо итератора!
// Попытка разыменования не скомпилируется.Это защитный механизм времени компиляции — традиционный std::find молча вернул бы висячий итератор, что привело бы к неопределённому поведению во время выполнения. Тип-страж std::ranges::dangling превращает это в ошибку компиляции.
Предпочитайте алгоритмы std::ranges:: в новом коде — они безопаснее (нет несовпадающих итераторов, защита от висячих ссылок), читабельнее (передача контейнеров напрямую) и мощнее (проекции, концепты).
Используйте традиционные алгоритмы std::, когда:
- Ваша кодовая база нацелена на компиляторы до C++20
- Вам нужна конкретная перегрузка, которую ranges не предоставляют
- Вы работаете с парами итераторов из устаревших API
Примечание: алгоритмы std::ranges:: не являются прямой заменой. Некоторые возвращают другие типы (например, std::ranges::sort возвращает итератор конца отсортированного диапазона). Проверяйте документацию при миграции.
- C++20 Ranges позволяют передавать контейнеры непосредственно алгоритмам — больше никакого шаблонного кода с
begin()/end() - Проекции преобразуют элементы для сравнения без модификации исходных данных
- Концепты диапазонов (
input_range,random_access_rangeи т.д.) обеспечивают чёткие ограничения времени компиляции - Оператор pipe
|позволяет создавать компонуемые конвейеры обработки данных слева направо std::ranges::danglingпредотвращает ошибки висячих итераторов на этапе компиляции- Предпочитайте алгоритмы
std::ranges::в новом коде C++20 для безопасности, читаемости и мощности
Тест — проверьте свои знания
(20 XP)1. Что такое проекция в контексте алгоритмов C++20 Ranges?
2. Что возвращает `std::ranges::find` при вызове с временным контейнером?
3. Что делает `std::ranges::sort(vec, std::ranges::greater{}, &Employee::salary)`?