Использование C++20 Coroutines для генерации интервалов

Попробовал использовать C++20 coroutines для генерации интервалов. Выглядит всё довольно неплохо. Вот небольшой пример использования.

Пусть у нас есть класс, содержащий внутри себя вектор уникальных указателей на векторы целых чисел, и некоторые из этих указателей могут быть null. Надо вернуть из функции класса интервал, содержащий все элементы ненулевых векторов и какую-нибудь заданную последовательность (например 10 единиц) для нулевых векторов:

class my_class {
public:
    auto data() const {
        // ???
    }
private:
    std::vector<std::unique_ptr<std::vector<int>>> data_;
};

С помощью одной лишь библиотеки ranges эта проблема не имеет простого решения. У нас есть адаптер std::ranges::views::join, который объединяет интервал интервалов в один интервал, но проблема в том, что интервалы в объединяемой последовательности должны иметь один и тот же тип, а у нас тип выбирается во время выполнения.

static std::array<int, 10> once_array;

auto data() const {
    auto fn = [](auto && vptr) {
        if (vptr) {
            // return type is std::ranges::ref_view<std::vector<int>>
            return (*vptr.get()) | std::ranges::views::all;
        } else {
            // return type is std::ranges::ref_view<std::array<int, 10>>
            return ones_array | std::ranges::views::all;
        }   
    };  
    auto data_ranges = data_ | std::ranges::views::transform(fn);
    return data_ranges | std::ranges::views::join;
}

Здесь помог бы какой-нибудь адаптер std::ranges::views::conditional, который во время выполнения выбирает один из двух интервалов, возвращаемых функторами, но такого адаптера нету. Поэтому придётся вручную реализовывать собственный итератор, который будет ходить по вектору указателей.

С помощью корутин эта проблема решается гораздо проще. В стандарте C++20 определены только ключевые слова для поддержки корутин и описаны концепции классов, которые эти корутины должны возвращать, но самих классов в стандартной библиотеке нету (их планируют добавить в стандарт C++23). Поэтому нужно взять одну из библиотек, имеющих классы для генерации интервалов, например ranges-v3, которая имеет шаблонный класс ranges::experimental::generator.

Функция data реализуется следующим образом:

ranges::experimental::generator<int> data() const {
    for (auto && vptr : data_) {
        if (vptr) {
            for (int i : *vptr) {
                co_yield i;
            }   
        } else {
            for (int i = 0; i < 10; ++i) {
                co_yield 1;
            }   
        }   
    }
}

Всё просто и понятно, никаких адских последовательностей адаптеров и трансформеров, и не надо писать свой собственный итератор.

С помощью корутин можно генерировать и модифицируемые последовательности. Для этого корутина должна возвращать ranges::experimental::generator<int &>. Но для нашего примера это неактуально, потому что непонятно что возвращать в случае нулевого вектора.

P. S. На самом деле у использования корутин для генерации интервалов есть один недостаток. Корутина всегда возвращает интервал, соответствующий концептам std::ranges::input_range или std::ranges::output_range, которые соответствуют обобщенному генератору последовательности. Интервалы C++20 гораздо мощнее и позволяют их использовать со сложными алгоритмами и генерировать более эффективный код.

comments powered by Disqus