Why does the resulting range iterator satisfy `forward_range` but the iterator has `input_iterator_tag`?
12:08 17 Jul 2026

The code snippet:

auto v = std::vector{1, 2, 3, 4, 5};

auto r =  v |
  std::views::transform([](int i) { return i - 42; });

static_assert(std::ranges::forward_range);
using I = std::ranges::iterator_t;
static_assert(std::forward_iterator);

// this fails
// static_assert(std::derived_from::iterator_category,
//                                 std::forward_iterator_tag>);

// WTF?
static_assert(std::same_as::iterator_category,
                           std::input_iterator_tag>);
static_assert(std::same_as);

https://godbolt.org/z/WTdxvY51d

The doc says std::views::transform gives forward_range if the base range models it, bidirectional_range if the base is bidirectional, etc.

The curious thing I don't understand is that the resulting range satisfies forward_range concept, its iterator satisfies forward_iterator concept (which contains the requirement std::derived_from, std::forward_iterator_tag>),
but the actual iterator category is std::input_iterator_tag.

Why the discrepancy?

The behavior is the same across major standard library implementations.

c++ c++20 std-ranges