c++std-ranges

Why does my transformed range not have a valid iterator?


I want a view of the characters from an input stream:

    auto input = std::stringstream{"abcd"};
    using Iter = std::istreambuf_iterator<char>;
    auto s = std::ranges::subrange{Iter{input}, Iter{}};

So far, so good. Now, I transform that view (using an identity transform for simplicity):

    auto t = s | std::views::transform(std::identity{});

Although this transformed view has a valid value_type (i.e. this assertion passes):

    using T = decltype(t);
    static_assert(std::is_same_v<char, std::ranges::range_value_t<T>>);

the iterator type from it doesn't have one:

    static_assert(std::is_same_v<char, std::iterator_traits<std::ranges::iterator_t<T>>::value_type>);

This fails with

view.cc: In function 'int main()':
view.cc:27:90: error: 'value_type' is not a member of 'std::iterator_traits<std::ranges::transform_view<std::ranges::subrange<std::istreambuf_iterator<char, std::char_traits<char> >, std::istreambuf_iterator<char, std::char_traits<char> >, std::ranges::subrange_kind::unsized>, std::identity>::_Iterator<false> >'
   27 |     static_assert(std::is_same_v<char, std::iterator_traits<std::ranges::iterator_t<T>>::value_type>);
      |                                                                                          ^~~~~~~~~~
view.cc:27:100: error: template argument 2 is invalid
   27 |     static_assert(std::is_same_v<char, std::iterator_traits<std::ranges::iterator_t<T>>::value_type>);
      |                   

The reason that it is important to me is that I want to use the iterator traits in a downstream view adapter.


Investigation suggests that the transform view's iterator is missing its iterator_category member. The code succeeds when I pass a more capable range such as std::string to the transform, but fails with this non-forward range.


Full code (also on Compiler Explorer):

#include <functional>
#include <iterator>
#include <ranges>
#include <sstream>
#include <string>
#include <type_traits>

int main()
{
#ifdef PASS
    auto input = std::string{"abcd"};
    std::ranges::forward_range auto s = std::ranges::subrange(input.begin(), input.end());
#else
    auto input = std::stringstream{"abcd"};
    using Iter = std::istreambuf_iterator<char>;
    std::ranges::input_range auto s = std::ranges::subrange{Iter{input}, Iter{}};
#endif

    using S = decltype(s);
    static_assert(std::is_same_v<char, std::ranges::range_value_t<S>>);
    static_assert(std::is_same_v<char, std::iterator_traits<std::ranges::iterator_t<S>>::value_type>);

    auto t = s | std::views::transform(std::identity{});
    using T = decltype(t);
    static_assert(std::is_same_v<char, std::ranges::range_value_t<T>>);
    static_assert(std::is_same_v<char, std::ranges::iterator_t<T>::value_type>);
    static_assert(std::is_base_of_v<std::input_iterator_tag, std::ranges::iterator_t<T>::iterator_category>);
    static_assert(std::is_same_v<char, std::iterator_traits<std::ranges::iterator_t<T>>::value_type>);
}

Solution

  • Investigation suggests that the transform view's iterator is missing its iterator_category member.

    When transform_view is applied to an underlying range, its iterator's post-increment operator (operator++(int)) returns a copy of it only when the underlying range models forward_range (which is reasonable since pre-incrementing the iterator of input_range invalidates all copies of it).

    Since the subrange of istreambuf_iterator is just an input_range, transform_view's iterator's post-increment operator returns nothing (void), which does not meet the requirements of Cpp17InputIterator, which requires that *it++ to be well-formed, i.e., the return type of the post-increment operator should be dereferenceable.

    So in your example, transform_view's iterator is not a valid C++17 input iterator, it's only a C++17 output iterator, which makes iterator_traits has no value_type member.