c++containersc++20std-rangesistream-view

How can I collect an istream_view into a container?


I was trying to implement a generic reduction operation for my extensions for 's ranges that would collect the elements of any range into a given container. To achieve that, I first created a dummy type for extracting a template template parameter and provided operator| for combing a range with it:

template <template <typename> typename T>
struct to_fn { };

template <template <typename> typename T>
inline constexpr detail::functors::to_fn<T> to;

template <template <typename> typename T>
auto operator|(std::ranges::range auto&& rng, detail::functors::to_fn<T>) {
    return T(std::ranges::begin(rng), std::ranges::end(rng));
}

Tested as follows:

int main() {
    using namespace std::ranges;
    std::vector<int> vec = {1, 2, 3, 4, 5};
    auto set = vec | to<std::set>;
    static_assert(std::same_as<decltype(set), std::set<int>>);
    assert(equal(vec, set));
}

the code finished execution with no problem.

However, the code fails to compile when used with std::ranges::istream_view:

int main() {
    using namespace std::ranges;
    std::ifstream input_file("input.txt");
    auto vec = istream_view<int>(input_file) | to<std::vector>;
}

This fails to compile with a wall of errors, among which, in my opinion, the important one being:

note:   deduced conflicting types for parameter '_InputIterator' ('std::ranges::basic_istream_view<int, char, std::char_traits<char> >::_Iterator' and 'std::default_sentinel_t')
122 |   return T(std::ranges::begin(rng), std::ranges::end(rng));
    |          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This makes sense to me. Containers require that the iterators used to construct them via constructors taking two of them are of the same type.

But that's fine - that's what std::ranges::views::common_view was created for. So I tried to modifying operator| to:

template <template <typename> typename T>
auto operator|(std::ranges::range auto&& rng, detail::functors::to_fn<T>) {
    auto common = rng | std::ranges::views::common;
    return T(std::ranges::begin(common), std::ranges::end(common));
}

Which, again, failed to compile with a smaller wall of errors, among which I believe this one is the most relevant:

note: the expression 'is_constructible_v<_Tp, _Args ...> [with _Tp = std::ranges::basic_istream_view<int, char, std::char_traits<char> >::_Iterator<int, char, std::char_traits<char> >; _Args = {std::ranges::basic_istream_view<int, char, std::char_traits<char> >::_Iterator<int, char, std::char_traits<char> >&}]' evaluated to 'false'
139 |       = destructible<_Tp> && is_constructible_v<_Tp, _Args...>;
    |                              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

I don't quite get what this error indicates, but I suppose this means that istream_view cannot be copy-constructed. Kind of makes sense to me.


But I really wish I could have this generic to "functor". I figured that it's okay to loop over istream_view with range-based for and add elements to the chosen container, when we deduce that we are dealing with an input range1.

So I tried this:

template <template <typename> typename T>
auto operator|(std::ranges::range auto&& rng, detail::functors::to_fn<T>) {
    using namespace std::ranges;
    using range_t = decltype(rng);
    const bool input_range = std::is_same_v<
            iterator_t<range_t>::iterator_category,
            std::input_iterator_tag>;
    if constexpr(input_range) {
        auto container = T<range_value_t<range_t>>();
        for (auto&& element : rng) {
            container.generic_add(element); // ???
        }
        return container;
    } else {
        auto common = rng | views::common;
        return T(begin(common), end(common));
    }
}

Which then told me, among other things, that:

error: 'iterator_category' is not a member of 'std::ranges::iterator_t<std::ranges::basic_istream_view<int, char, std::char_traits<char> >&&>'
125 |             iterator_t<range_t>::iterator_category,
    |                                  ^~~~~~~~~~~~~~~~~

This is not the only problem. There is also a problem of generically adding elements to any container. The constructor taking a range is, to my knowledge, the only generic way and good way of adding elements to the container.

I feel like there must be a correct and simpler way of doing what I am trying to do. Bonus points if to also works for non-templates, i.e., I could not only do to<std::vector> but also to<std::string>. In the first case, it would deduce the elements and create the desired instantiation of std::vector, but in the second case it would take all elements and initialise an std::string with those. How can I make this work?


1 This assumes that the actual problem lies in the fact that we are using input range. I am not sure whether that's the case. I would love if someone could point out the possible error in my reasoning.


Solution

  • For , ranges::to (P1206R7) will correctly handle move-only views.


    This assumes that the actual problem lies in the fact that we are using input range.

    The problem isn't that it's an input range, the problem is that std::ranges::istream_view<int>, along with its iterator type, are move-only.

    In C++17, all iterators had to be copyable. That restriction was relaxed in C++20, we can now have move-only iterators and move-only views. But code has to change to support that - std::vector's iterator-pair constructor is still based on the C++17 iterator model, which does copy iterators around, so it cannot work for std::ranges::istream_view<int>::iterator.

    The point of common_view is to take a C++20 range, one that has a sentinel type that differs from its iterator type, and adapt it to work with C++17 algorithms by producing the same type for both the iterator and sentinel. But the point here is to work with C++17 algorithms, and so it has to adhere to C++17 iterator requirements. Those requirements include copyability, so common_view is declared as, from [range.common.view]:

    namespace std::ranges {
      template<view V>
        requires (!common_range<V> && copyable<iterator_t<V>>)
      class common_view : public view_interface<common_view<V>> {
    

    That's why rng | views::common doesn't compile for you, common requires copyability (in the case where the iterator/sentinel type differ, as they do here) and you don't have that (also you should be forwarding rng, since even if views::common didn't require copyability directly, what you're doing is actually copying it, so would fail separately).

    There's really no way to adapt std::ranges::istream_view<int> such that you can use vector<int>'s iterator-pair constructor. We would need to either change vector in some way to allow this construction to work (which certainly doesn't help you right now), or you have to handle this case by doing the equivalent of:

    std::vector<int> v;
    for (int e : rng) {
        v.push_back(e);
    }
    return v;
    

    Though that has to look like s.insert(e) for the std::set case.