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>);
}
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.