STL

std::ranges в C++20: декларативная обработка коллекций

Введение

Классический стиль работы с алгоритмами 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-переменные с понятными именами.

================================================================================