Search code examples
c++std-ranges

Consume InputIterator with C++ ranges


With an input iterator, I can consume different sub-ranges out of it, e.g.

void test(std::input_iterator auto it) {
  while (*it < 1) ++it; // drop_while
  int n = *it; ++it; // take(1)
  int sum = 0; for (; n-- > 0; ++it) sum += (1 + *it); // fold_left(take(n) | transform)
  int prd = 1; for (; int x = *it / 2; ++it) prd *= x; // fold_left(transform | take_while)
  std::cout << sum << ' ' << prd << '\n';
}

int main() {
    test(std::begin({0, 0, 0, 3, 400, 30, 2, 4, 6, 0}));
}

Is there a way to do the same with std::ranges/std::views?


Solution

  • The problem is that in algo(range | view_with_a_sentinel), the algorithm doesn't receive the original range, so it cannot return the unconsumed part of the range.

    However, with all those views, and ranges, deep inside there is an iterator which gets incremented. If we get this iterator, we can construct another range.

    So the answer is: use indirection.

    Let's write an input iterator that has a reference to another input iterator.

    template<std::input_iterator I>
    struct IterProxy : std::iterator_traits<I> {
        std::reference_wrapper<I> it{};
        IterProxy(I& it) : it{it} {}
    
        using iterator_concept = std::input_iterator_tag;
    
        auto operator*() const { return *it.get(); }
        auto& operator++() { ++it.get(); return *this; }
        void operator++(int) { ++it.get(); }
    
        friend auto operator<=>(IterProxy, IterProxy) = default;
        friend auto operator==(IterProxy p, I i) { return p.it.get() == i; }
        friend auto operator==(I i, IterProxy p) { return p.it.get() == i; }
    };
    

    Now we can use this proxy instead of an actual range, so we would always keep track of an original input iterator:

    void test_ranges(std::ranges::input_range auto range) {
      auto it{std::ranges::begin(range)}, last{std::ranges::end(range)};
      auto tail = std::ranges::subrange(IterProxy{it}, last);
    
      algo1(tail | view | ...);
      algo2(tail | view | ...);
    }
    

    Here is the code from the question, with ranges:

    void test_ranges(std::ranges::input_range auto range) {
      auto it{std::ranges::begin(range)}, last{std::ranges::end(range)};
      auto tail = std::ranges::subrange(IterProxy{it}, last);
      int n = *std::ranges::begin(tail | std::views::drop_while([](auto x) { return x < 1; }));
      ++it; // could be begin(tail | take(1))
      int sum = std::ranges::fold_left(
          tail | std::views::take(n) | std::views::transform([](auto x) { return x + 1; }),
          0, [](auto sum, auto x) { return sum + x; });
      int prd = std::ranges::fold_left(
          tail | std::views::transform([](auto x) { return x / 2; }) | std::views::take_while([](auto x) { return x != 0; }),
          1, [](auto prd, auto x) { return prd * x; });
      std::cout << sum << ' ' << prd << '\n';
    }
    
    int main() {
      std::istringstream in{"0 0 0   3   400 30 2   4 6   0"};
      std::istream_iterator<int> first{in}, last;
      test_ranges(std::ranges::subrange{first, last});
    }
    

    https://godbolt.org/z/4zGscYvs4