Search code examples
c++stlstd-ranges

Update range on successive iterations based on strides (or other operations)


In the code below I can create the strode_view to store the view which strides the range so that I could work with it in different places (check the size, etc.). But I have to add a new variable while in real code the strode version should just “replace” the original range transparently.

I do striding for performance reason, so strode "version" is used only when data reaches some threshold.

Is there a chance to update the range (see in the last line of the code) so that this replacement is not visible for the code which will follow?

And if not, why?

The code

#include <execution>
#include <iostream>
#include <ranges>
#include <vector>

int main()
{
    std::vector<float> values(10);
    std::ranges::iota(values, 0);
    auto range = std::ranges::subrange(values.begin(),values.end());

    std::ranges::for_each(range, [](auto& v) { std::cout << v << " ";});
    std::cout << std::endl;

    const int in_range_i_need_only = 3;
    std::ranges::for_each(range | std::ranges::views::stride(range.size() / in_range_i_need_only), [](auto& v) { std::cout << v << " ";});

    std::cout << std::endl;

    auto strode_view = range | std::ranges::views::stride(range.size() / in_range_i_need_only);
    std::ranges::for_each(strode_view, [](auto& v) { std::cout << v << " ";});

    // No way to assign here; is there a workaround?
    range = range | std::ranges::views::stride(range.size() / in_range_i_need_only);
}

The demo

Per comments, the problem with the code above is that the type (and the essence) of the first range (on the right side of the assignment) (which is range) and second range (on the left side of the assignment) (which is stride_view over range) is different.

So I tried to get the view from the beginning to make the assignment possible (last 4 lines of code reworked to):

    auto view = std::ranges::subrange(range);
    std::ranges::for_each(view, [](auto& v) { std::cout << v << " "; });

    // No way to assign here; is there a workaround?
    view = view | std::ranges::views::stride(range.size() / in_range_i_need_only);

Unfortunately, this doesn't work either because although view must be a view by std::ranges::subrange

The subrange class template combines together an iterator and a sentinel into a single view.

It is not and it is still a range and the left side of the last assignment must be std::ranges::stride_view<std::ranges::subrange<...

So, even the trick with views doesn't work, since the type is growing with every view pipe operation applied.

The difference is not only between range and view, but between types of each and every view view after pipeline operation application.

Is there a way to manage this and stay with one variable only without being forced to check which of ranges/views to use in the code which will follow. I need the code following later doesn't care about the changes in the range/view. Is this possible?


Solution

  • It's already largely stated in the comments, but the answer for the case where every evolution of the range in question is some stride over some subrange of the same base range (or at least the same type of base range) is

    void whittle(auto r) {
        int a = 0, b = std::ranges::size(r), s = 1;
        auto mk = [&] {
            return std::ranges::subrange(std::ranges::begin(r) + a,
                                         std::ranges::begin(r) + b) |
                   std::views::stride(s);
        };
        for (auto v = mk(); !v.empty(); ++a, b -= b > a, ++s, v = mk()) {
            for (auto &&x : v) std::cout << x;
            std::cout << '\n';
        }
    }
    

    Note that the subrange and stride must be used even if they do not restrict the range. Note also that, because the type of the view is (as always) cumbersome to write out, we end up using a lambda to avoid duplication; furthermore, for many algorithms we don't need to actually assign to the view because there is only the one expression for it. The above can be written more clearly as

    void whittle(auto r) {
        for (int a = 0, b = std::ranges::size(r), s = 1;; ++a, b -= b > a, ++s) {
            const auto v = std::ranges::subrange(std::ranges::begin(r) + a,
                                                 std::ranges::begin(r) + b) |
                           std::views::stride(s);
            if (v.empty()) break;
            for (auto &&x : v) std::cout << x;
            std::cout << '\n';
        }
    }
    

    Other parametrized views like std::ranges::drop_view could be used similarly; changing the type of the view does of course require some form of type erasure. The type erasure might not apply to the view itself: a std::ranges::transform_view could be used with various transformations wrapped in the same type of std::function, for instance.