Введение
Классический стиль работы с алгоритмами STL — std::sort(v.begin(), v.end()) — требует постоянно передавать пары итераторов, что многословно и подвержено ошибкам (легко перепутать диапазоны двух разных контейнеров). C++20 вводит библиотеку std::ranges, которая позволяет работать с контейнерами напрямую, без явных итераторов, и вводит views — ленивые, компонуемые преобразования диапазонов, похожие на конвейеры в функциональных языках.
Концепция
std::ranges — это переработка алгоритмов STL, которые теперь могут принимать сам контейнер (range), а не пару итераторов: std::ranges::sort(v) вместо std::sort(v.begin(), v.end()).
Views (std::views::filter, std::views::transform, std::views::take, и т.д.) — это адаптеры, которые не копируют и не материализуют данные сразу, а вычисляют их лениво, при обходе. Их можно соединять в цепочки через operator|, аналогично конвейерам в Unix-шелле или LINQ в C#.
Пример кода
#include <ranges>
#include <algorithm>
#include <vector>
#include <iostream>
#include <string>
struct Product {
std::string name;
double price;
bool inStock;
};
int main() {
std::vector<Product> products = {
{"Laptop", 1200.0, true},
{"Mouse", 25.0, true},
{"Monitor", 300.0, false},
{"Keyboard", 45.0, true},
{"Webcam", 60.0, false},
{"Headset", 80.0, true}
};
namespace rv = std::views;
// Декларативный конвейер: товары в наличии дешевле 100, их названия, первые 2
auto cheapInStockNames =
products
| rv::filter([](const Product& p) { return p.inStock && p.price < 100.0; })
| rv::transform([](const Product& p) { return p.name; })
| rv::take(2);
std::cout << "Cheap in-stock (first 2): ";
for (const auto& name : cheapInStockNames) {
std::cout << name << " ";
}
std::cout << "n";
// ranges-алгоритмы без явных итераторов
std::ranges::sort(products, [](const Product& a, const Product& b) {
return a.price < b.price;
});
std::cout << "Sorted by price: ";
for (const auto& p : products) {
std::cout << p.name << "(" << p.price << ") ";
}
std::cout << "n";
// ranges::find_if напрямую с контейнером
auto it = std::ranges::find_if(products, [](const Product& p) {
return p.name == "Monitor";
});
if (it != products.end()) {
std::cout << "Found: " << it->name << "n";
}
// Подсчёт через views без промежуточных контейнеров
auto inStockCount = std::ranges::count_if(products, &Product::inStock);
std::cout << "In stock count: " << inStockCount << "n";
return 0;
}
Пояснения к коду
Конвейер products | rv::filter(...) | rv::transform(...) | rv::take(2) читается практически как описание задачи на естественном языке: «отфильтровать товары в наличии дешевле 100, взять их названия, оставить первые два». При этом ни один промежуточный контейнер не создаётся — filter, transform и take возвращают lazy views, которые вычисляются только при итерировании финального результата в for.
std::ranges::sort(products, ...) принимает сам вектор, а не пару итераторов — это устраняет распространённый класс ошибок, когда по неосторожности передаются begin() одного контейнера и end() другого.
std::ranges::count_if(products, &Product::inStock) демонстрирует ещё одну приятную возможность ranges-алгоритмов — projections: вместо лямбды [](const Product& p){ return p.inStock; } можно напрямую передать указатель на член класса, и алгоритм сам «спроецирует» каждый элемент через этот член.
Подводные камни и советы
1. Views ленивы — выражение products | rv::filter(...) само по себе не выполняет фильтрацию; вычисление происходит при обходе. Если нужно зафиксировать результат, материализуйте его явно, например через std::ranges::to<std::vector>() (C++23) или цикл с push_back. 2. Время жизни диапазона: view хранит ссылку (или итераторы) на исходный контейнер. Если контейнер изменится или будет уничтожен до использования view — получите undefined behavior. Не возвращайте view, построенный на основе локальной временной переменной. 3. Не все алгоритмы STL имеют ranges-версию в одной и той же библиотеке стандартов — проверяйте поддержку компилятора (GCC, Clang, MSVC по-разному и в разное время реализовали <ranges>). 4. Используйте std::ranges и views там, где раньше писалась цепочка временных векторов «сначала отфильтровать, потом преобразовать, потом взять первые N» — это уменьшает количество промежуточных аллокаций и делает код декларативным и читаемым. 5. Помните, что чрезмерно длинные цепочки views могут затруднить чтение типов в сообщениях об ошибках компилятора — в сложных случаях разбивайте цепочку на промежуточные auto-переменные с понятными именами.
================================================================================