c++c++20std-ranges

Using range-based for loops and range adaptors with non-copyable input iterators


While the legacy C++17 named requirement InputIterator requires the iterator to be copyable, the new C++20 concept std::input_iterator does not.

But I tried to use std::views::drop(1) on a custom range that uses non-copyable iterators, and it didn't compile, issuing errors about my InputIterator not being copyable. So it seems that in fact a range does require their iterators to be copyable.

Minimal example:

#include <iterator>
#include <ranges>
#include <iostream>

class InputIterator {
public:
    using difference_type = std::ptrdiff_t;
    using value_type = int; 
    
    // Movable
    InputIterator(InputIterator&&) = default;
    InputIterator& operator=(InputIterator&&) = default;
    
    // Non-copyable
    InputIterator(const InputIterator&) = delete;
    InputIterator& operator=(const InputIterator&) = delete;
    
    int operator*() const { return *p_; } 
    InputIterator& operator++() { ++p_; return *this; }
    void operator++(int) { ++*this; }
    
    // Not required for input_iterator concept, but used for the rest of the example
    InputIterator() = default;
    InputIterator(const int* p) : p_(p) {}
    bool operator==(const InputIterator& other) const = default;
    bool operator!=(const InputIterator& other) const = default;
    
private:  
    const int* p_ = {};
};
 
static_assert(std::input_iterator<InputIterator>);

class InputView : public std::ranges::view_interface<InputView> {
public:
    InputIterator begin() const { return {array}; };   // RVO so OK even if non-copyable
    InputIterator end() const { return {array + 4}; }; // RVO so OK even if non-copyable
    
private:
    int array[4] = {1, 2, 3, 4};
};

int main() {
    // OK
    InputView view;

    // OK
    for (int x : view) {
        std::cout << x << " ";
    }
    
    // Does not compile
    for (int x : view | std::views::drop(1)) {
        std::cout << x << " ";
    }

    // Does not compile
    static_assert(std::ranges::input_range<InputView>);
}

The compile errors include things like:

[...]
note: because type constraint 'sentinel_for<InputIterator, __range_iter_t<InputView &> >' was not satisfied:
note: because 'InputIterator' does not satisfy 'semiregular'
note: because 'InputIterator' does not satisfy 'copyable'
note: because 'InputIterator' does not satisfy 'copy_constructible'
[...]

Is there a workaround to be able to use range adaptors (such as drop) with non-copyable iterators?

Related questions:

  1. Does anyone know why input_range requires the underlying input_iterator to be copyable, while input_iterator itself does not?
  2. Is it bad practice to write generic algorithms that only support input_range and not input_iterator, since the former is more restricted?
  3. In practice, are there actually input_iterator out there that are not copyable?

There is a related SO post:

In a C++ range-based for loop, can begin() return a reference to a non-copyable iterator?

But it only considers range-based loops without adaptors (we can see in the example above that this is no problem), and doesn't really address why input_range seems to require copyability, which makes adaptors not work with non-copyable input iterators.


Solution

    1. Does anyone know why input_range requires the underlying input_iterator to be copyable, while input_iterator itself does not?

    It doesn't actually. input_range<R> requires that:

    1. Let I be the type of ranges::begin(r). I has to model input_iterator.
    2. Let S be the type of ranges::end(r). S has to model sentinel_for<I>.

    Note that for end(), we're not checking that the type is an iterator at all. We're just checking that it's a sentinel for the iterator type.

    In your case (indeed in the common case, which is why they're called common_ranges), your begin() and end() return the same type, and so we are checking both that your InputIterator models input_iterator (which does not require copyable) and sentinel_for<InputIterator> (which, in addition to the appropriate comparisons, also requires semiregular which requires copyable).

    So for InputView to actually be a range, you would need to either make your iterator copyable or return a sentinel type from end().

    1. Is it bad practice to write generic algorithms that only support input_range and not input_iterator, since the former is more restricted?

    I think this question is just a misunderstanding.

    1. In practice, are there actually input_iterator out there that are not copyable?

    Yes. Indeed, that's why iterators are allowed to be move-only. The typical input iterator probably should be move-only - since the whole premise of an input-only range is that you can only represent one position at a time. For instance, the iterators from views::istream or std::generator, etc, are move-only.