Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't I use istream_view and std::accumulate to sum up my input?

This program:

#include <ranges>
#include <numeric>
#include <iostream>

int main() {
    auto rng = std::ranges::istream_view<int>(std::cin);
    std::cout << std::accumulate(std::ranges::begin(rng), std::ranges::end(rng), 0);
}

is supposed to sum up all integers appearing as text on the standard input stream. But - it doesn't compile. I know std::ranges::begin() and std::ranges::end() exist, so what's going on? The compiler tells me it can't find a suitable candidate; why?

like image 215
einpoklum Avatar asked Dec 15 '20 19:12

einpoklum


2 Answers

From inception up through C++17, everything in <algorithm> is based on iterator pairs: you have one iterator referring to the beginning of a range and one iterator referring to the end of the range, always having the same type.

In C++20, this was generalized. A range is now denoted by an iterator and a sentinel for that iterator - where the sentinel itself need not actually be an iterator of any kind, it just needs to be a type that can compare equal to its corresponding iterator (this is the sentinel_for concept).

C++17 ranges tend to be valid C++20 ranges, but not necessarily in the opposite direction. One reason is the ability to have a distinct sentinel type, but there are others, which also play into this question (see below).

To go along with the new model, C++20 added a large amount of algorithms into the std::ranges namespace that take an iterator and a sentinel, rather than two iterators. So for instance, while we've always had:

template<class InputIterator, class T>
  constexpr InputIterator find(InputIterator first, InputIterator last,
                               const T& value);

we now also have:

namespace ranges {
  template<input_­iterator I, sentinel_­for<I> S, class T, class Proj = identity>
    requires indirect_­binary_­predicate<ranges::equal_to, projected<I, Proj>, const T*>
    constexpr I find(I first, S last, const T& value, Proj proj = {});

  template<input_­range R, class T, class Proj = identity>
    requires indirect_­binary_­predicate<ranges::equal_to,
                                       projected<iterator_t<R>, Proj>, const T*>
    constexpr borrowed_iterator_t<R>
      find(R&& r, const T& value, Proj proj = {});
}

The first overload here takes an iterator/sentinel pair and the second takes a range instead.


While a lot of algorithms added corresponding overloads into std::ranges, the ones in <numeric> were left out. There is a std::accumulate but there is no std::ranges::accumulate. As such, the only version we have available at the moment is one that takes an iterator-pair. Otherwise, you could just write:

auto rng = std::ranges::istream_view<int>(std::cin);
std::cout << std::ranges::accumulate(rng, 0);

Unfortunately, std::ranges::istream_view is one of the new, C++20 ranges whose sentinel type differs from its iterator type, so you cannot pass rng.begin() and rng.end() into std::accumulate either.

This leaves you with two options generally (three, if you include waiting for C++23, which will hopefully have a std::ranges::fold):

  1. Write your own range-based and iterator-sentinel-based algorithms. Which for fold is very easy to do.

Or

  1. There is a utility to wrap a C++20 range into a C++17-compatible one: views::common. So you could this:
auto rng = std::ranges::istream_view<int>(ints) | std::views::common;
std::cout << std::accumulate(rng.begin(), rng.end(), 0);

Except not in this specific case.

istream_view's iterators aren't copyable, and in C++17 all iterators must be. So there isn't really a way to provide C++17-compatible iterators based on istream_view. You need proper C++20-range support. The future std::ranges::fold will support move-only views and move-only iterators, but std::accumulate never can.

Which in this case, just leaves option 1.


A C++20 iterator needs to be default-constructible, which was not a requirement of C++17 iterators. So a C++17 range with non-default-constructible iterators would not be a valid C++20 range.

like image 170
Barry Avatar answered Sep 25 '22 16:09

Barry


The problem is that the end of a C++ range is not, in the general case, an iterator, but rather, a sentinel. A sentinel can have a different type than an iterator and admit fewer operations - as, generally speaking, you mostly need to compare against it to know you've reached the end of the range, and may not be allowed to just work with it like any iterator. For more about this distinction, read:

What's the difference between a sentinel and an end iterator?

Now, standard-library algorithms (including the ones in <numeric>) take pairs of iterators of the same type. In your example:

template< class InputIt, class T >
constexpr T accumulate( InputIt first, InputIt last, T init );

see? InputIt must be the type of both the beginning and end of the range. This can (probably) not even be "fixed" for the istream_view range, because the end of the standard input really isn't an iterator per se. (Although maybe you could bludgeon it into being an iterator and throw exceptions when doing irrelevant things with it.)

So, we would need either a new variant of std::accumulate or an std::ranges::accumulate, which we currently don't have. Or, of course, you could write that yourself, which should not be too difficult.

Edit: One last option, suggested by @RemyLebeau, is to use an std::istream_iterator instead:

std::cout << std::accumulate(
    std::istream_iterator<int>(std::cin), 
    std::istream_iterator<int>(), 
    0);
like image 29
einpoklum Avatar answered Sep 25 '22 16:09

einpoklum