I was trying to implement a generic reduction operation for my extensions for c++20's ranges
that would collect the elements of any range
into a given container. To achieve that, I first created a dummy type for extracting a template template
parameter and provided operator|
for combing a range
with it:
template <template <typename> typename T>
struct to_fn { };
template <template <typename> typename T>
inline constexpr detail::functors::to_fn<T> to;
template <template <typename> typename T>
auto operator|(std::ranges::range auto&& rng, detail::functors::to_fn<T>) {
return T(std::ranges::begin(rng), std::ranges::end(rng));
}
Tested as follows:
int main() {
using namespace std::ranges;
std::vector<int> vec = {1, 2, 3, 4, 5};
auto set = vec | to<std::set>;
static_assert(std::same_as<decltype(set), std::set<int>>);
assert(equal(vec, set));
}
the code finished execution with no problem.
However, the code fails to compile when used with std::ranges::istream_view
:
int main() {
using namespace std::ranges;
std::ifstream input_file("input.txt");
auto vec = istream_view<int>(input_file) | to<std::vector>;
}
This fails to compile with a wall of errors, among which, in my opinion, the important one being:
note: deduced conflicting types for parameter '_InputIterator' ('std::ranges::basic_istream_view<int, char, std::char_traits<char> >::_Iterator' and 'std::default_sentinel_t') 122 | return T(std::ranges::begin(rng), std::ranges::end(rng)); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This makes sense to me. Containers require that the iterators used to construct them via constructors taking two of them are of the same type.
But that's fine - that's what std::ranges::views::common_view
was created for. So I tried to modifying operator|
to:
template <template <typename> typename T>
auto operator|(std::ranges::range auto&& rng, detail::functors::to_fn<T>) {
auto common = rng | std::ranges::views::common;
return T(std::ranges::begin(common), std::ranges::end(common));
}
Which, again, failed to compile with a smaller wall of errors, among which I believe this one is the most relevant:
note: the expression 'is_constructible_v<_Tp, _Args ...> [with _Tp = std::ranges::basic_istream_view<int, char, std::char_traits<char> >::_Iterator<int, char, std::char_traits<char> >; _Args = {std::ranges::basic_istream_view<int, char, std::char_traits<char> >::_Iterator<int, char, std::char_traits<char> >&}]' evaluated to 'false' 139 | = destructible<_Tp> && is_constructible_v<_Tp, _Args...>; | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
I don't quite get what this error indicates, but I suppose this means that istream_view
cannot be copy-constructed. Kind of makes sense to me.
But I really wish I could have this generic to
"functor". I figured that it's okay to loop over istream_view
with range-based for
and add elements to the chosen container, when we deduce that we are dealing with an input range1.
So I tried this:
template <template <typename> typename T>
auto operator|(std::ranges::range auto&& rng, detail::functors::to_fn<T>) {
using namespace std::ranges;
using range_t = decltype(rng);
const bool input_range = std::is_same_v<
iterator_t<range_t>::iterator_category,
std::input_iterator_tag>;
if constexpr(input_range) {
auto container = T<range_value_t<range_t>>();
for (auto&& element : rng) {
container.generic_add(element); // ???
}
return container;
} else {
auto common = rng | views::common;
return T(begin(common), end(common));
}
}
Which then told me, among other things, that:
error: 'iterator_category' is not a member of 'std::ranges::iterator_t<std::ranges::basic_istream_view<int, char, std::char_traits<char> >&&>' 125 | iterator_t<range_t>::iterator_category, | ^~~~~~~~~~~~~~~~~
This is not the only problem. There is also a problem of generically adding elements to any container. The constructor taking a range
is, to my knowledge, the only generic way and good way of adding elements to the container.
I feel like there must be a correct and simpler way of doing what I am trying to do. Bonus points if to
also works for non-templates, i.e., I could not only do to<std::vector>
but also to<std::string>
. In the first case, it would deduce the elements and create the desired instantiation of std::vector
, but in the second case it would take all elements and initialise an std::string
with those. How can I make this work?
1 This assumes that the actual problem lies in the fact that we are using input range. I am not sure whether that's the case. I would love if someone could point out the possible error in my reasoning.
For c++23,
ranges::to
(P1206R7) will correctly handle move-only views.The problem isn't that it's an input range, the problem is that
std::ranges::istream_view<int>
, along with its iterator type, are move-only.In C++17, all iterators had to be copyable. That restriction was relaxed in C++20, we can now have move-only iterators and move-only views. But code has to change to support that -
std::vector
's iterator-pair constructor is still based on the C++17 iterator model, which does copy iterators around, so it cannot work forstd::ranges::istream_view<int>::iterator
.The point of
common_view
is to take a C++20 range, one that has a sentinel type that differs from its iterator type, and adapt it to work with C++17 algorithms by producing the same type for both the iterator and sentinel. But the point here is to work with C++17 algorithms, and so it has to adhere to C++17 iterator requirements. Those requirements include copyability, socommon_view
is declared as, from [range.common.view]:That's why
rng | views::common
doesn't compile for you,common
requires copyability (in the case where the iterator/sentinel type differ, as they do here) and you don't have that (also you should be forwardingrng
, since even ifviews::common
didn't require copyability directly, what you're doing is actually copying it, so would fail separately).There's really no way to adapt
std::ranges::istream_view<int>
such that you can usevector<int>
's iterator-pair constructor. We would need to either changevector
in some way to allow this construction to work (which certainly doesn't help you right now), or you have to handle this case by doing the equivalent of:Though that has to look like
s.insert(e)
for thestd::set
case.