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:
input_range
requires the underlying input_iterator
to be copyable, while input_iterator
itself does not?input_range
and not input_iterator
, since the former is more restricted?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.
- Does anyone know why
input_range
requires the underlyinginput_iterator
to be copyable, whileinput_iterator
itself does not?
It doesn't actually. input_range<R>
requires that:
I
be the type of ranges::begin(r)
. I
has to model input_iterator
.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_range
s), 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()
.
- Is it bad practice to write generic algorithms that only support
input_range
and notinput_iterator
, since the former is more restricted?
I think this question is just a misunderstanding.
- 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.