Search code examples
c++std-rangesc++23

Preserving std::forward_range during chunk transformation?


encode below is a dumbed down version of a transformation I am working on (run it on godbolt):

#include <array>
#include <iostream>
#include <ranges>

int main() {
    static constexpr auto arr = std::array{1, 2, 3, 4, 5, 6, 7};
    static constexpr auto encode = std::views::chunk(3) |
                                   std::views::transform([](auto chunk) {
                                       std::array<int, 4> a{};
                                       auto i = 0;
                                       for (const auto j : chunk) {
                                           a.at(i++) = j;
                                       }
                                       a.at(3) = a.at(0);
                                       return a;
                                   }) |
                                   std::views::join;
    auto encoded = arr | encode;

    static_assert(std::ranges::contiguous_range<decltype(arr)>);
    static_assert(std::ranges::input_range<decltype(encoded)>);
    static_assert(not std::ranges::forward_range<decltype(encoded)>);

    for (const auto i : encoded) {
        std::cout << i;
    }

    std::cout << '\n';

    return 0;
}

Output:

123145647007

As the static_asserts show, although the input is a contiguous_range, the output is not even a forward_range. It is not necessarily a problem for me, but as a matter of principle, I would like my adaptors to be as range property-preserving as possible. Is there an obvious way to rewrite encode to make it preserve forward_ranges or is that type of chunk transformation always going to break that property?


Edit, additional question:

If there is no obvious way, is there a non-obvious way, like one that would involve implementing the range adaptor from scratch?


Solution

  • As it turns out, the answer is yes, there is a way to rewrite encode to even preserve random_access_ranges and sized_ranges (run it on godbolt):

    #include <array>
    #include <iostream>
    #include <ranges>
    
    struct encode_fn : std::ranges::range_adaptor_closure<encode_fn> {
        template <std::ranges::viewable_range R>
            requires std::convertible_to<std::ranges::range_reference_t<R>, int>
        constexpr auto operator()(R &&r) const {
            return std::views::cartesian_product(
                       std::forward<R>(r) | std::views::chunk(3),
                       std::views::iota(0, 4)) |
                   std::views::transform([](auto indexed) -> int {
                       auto [v, i] = indexed;
                       if (i == 3) {
                           return v[0];
                       }
                       if (i > std::ranges::size(v) - 1) {
                           return 0;
                       }
                       return v[i];
                   });
        }
    };
    
    int main() {
        static constexpr auto arr = std::array{1, 2, 3, 4, 5, 6, 7};
    
        static constexpr encode_fn encode{};
        auto encoded = arr | encode;
    
        static_assert(std::ranges::contiguous_range<decltype(arr)>);
        static_assert(std::ranges::random_access_range<decltype(encoded)>);
        static_assert(std::ranges::sized_range<decltype(encoded)>);
        static_assert(std::ranges::size(arr | encode) == 12);
    
        for (const auto i : encoded) {
            std::cout << i;
        }
    
        std::cout << '\n';
    
        return 0;
    }
    

    Same output. With the help of cartesian_product, we handle every chunk multiple times, producing one transformed chunk item at a time, thus removing the need to ever store intermediate chunks.