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.
to
than to manually SFINAE on insetion method, correct? – Repair