I wanted to create my own, pipeable split_when
and having fixed some initial problems, I managed to make this example work:
auto main() -> int {
auto v = std::vector<int>{1, 2, 3, 4, 5};
auto split = v | so::views::splitWhen([](int n) { return n % 2 == 0; });
for (auto range : split) {
for (auto e : range) {
std::cout << e << ' ';
}
std::cout << '\n';
}
}
This prints:
1 2
3 4
5
However, in the original, more complicated example, I wanted to avoid the inner loop, so I figured I would use std::views::join
auto main() -> int {
auto v = std::vector<int>{1, 2, 3, 4, 5};
auto split = v | so::views::splitWhen([](int n) { return n % 2 == 0; })
| std::views::join;
for (auto e : split) {
std::cout << e << ' ';
}
}
1 2 3 4 5
This worked well, until I wanted to abstract out the % 2
part. I extracted it to an external variable and captured it within a lambda:
auto main() -> int {
auto v = std::vector<int>{1, 2, 3, 4, 5};
auto divisor = 2;
auto split = v | so::views::splitWhen([divisor](int n) { return divisor % 2 == 0; })
| std::views::join;
for (auto e : split) {
std::cout << e << ' ';
}
}
This code no longer compiles and produces a 250 line error. Couple of (I believe) relevant parts of it:
error: no match for 'operator|' (operand types are 'so::views::SplitWhenView<std::ranges::ref_view<std::vector<int> >, main()::<lambda(int)> >' and 'const std::ranges::views::_Join')
note: 'std::ranges::views::__adaptor::_RangeAdaptorClosure' is not a base of 'so::views::SplitWhenView<std::ranges::ref_view<std::vector<int> >, main()::<lambda(int)> >'
The full implementation is as follows:
I am using GCC 13.1.0, which does not support std::bind_back()
. Thus, I have created my own, inferior implementation that was inspired by cppreference's possible implementation:
namespace so {
template <class ConstFn, class... Args>
constexpr auto bind_back(ConstFn fn, Args&& ... args) {
using F = ConstFn;
if constexpr (std::is_pointer_v<F> or
std::is_member_pointer_v<F>)
static_assert(fn != nullptr);
return
[fn, ... bound_args(
std::forward<Args>(args))]<class... T>
(
T &&... call_args
)
{
return std::invoke(fn, std::forward<T>(call_args)..., bound_args...);
};
}
}
I am using the following helper factories from P2387:
namespace so::views {
template <typename F>
class closure : public std::ranges::range_adaptor_closure<closure<F>> {
F f;
public:
constexpr closure(F f) : f(f) { }
template <std::ranges::viewable_range R>
requires std::invocable<F const&, R>
constexpr decltype(auto) operator()(R&& r) const {
return f(std::forward<R>(r));
}
};
template <typename F>
class adaptor {
F f;
public:
constexpr adaptor(F f) : f(f) { }
template <typename... Args>
constexpr decltype(auto) operator()(Args&&... args) const {
if constexpr (std::invocable<F const&, Args...>) {
return f(std::forward<Args>(args)...);
} else {
return closure(bind_back(f, std::forward<Args>(args)...));
}
}
};
}
My view and its iterator:
namespace so::views {
template <std::ranges::input_range View, std::predicate<std::ranges::range_value_t<View>> F> requires std::ranges::view<View>
class SplitWhenView
: public std::ranges::view_interface<SplitWhenView<View, F>> {
View base_;
F predicate_;
public:
SplitWhenView() = default;
SplitWhenView(View base, F predicate) : base_(std::move(base)), predicate_(std::move(predicate)) {}
auto base() { return base_; }
template <std::input_iterator It, std::predicate<std::ranges::range_value_t<View>> Predicate>
class SplitWhenIterator;
using iterator = SplitWhenIterator<std::ranges::iterator_t<View>, F>;
auto begin() { return SplitWhenIterator(std::ranges::begin(base_), std::ranges::end(base_), predicate_); }
auto end() { return SplitWhenIterator(std::ranges::end(base_), std::ranges::end(base_), predicate_); }
};
template <std::ranges::input_range View, std::predicate<std::ranges::range_value_t<View>> F> requires
std::ranges::view<View>
template <std::input_iterator It, std::predicate<std::ranges::range_value_t<View>> Predicate>
class SplitWhenView<View, F>::SplitWhenIterator {
It current_, end_;
Predicate predicate_;
public:
using value_type = std::ranges::subrange<It>;
using difference_type = std::ranges::range_difference_t<View>;
SplitWhenIterator() = default;
SplitWhenIterator(It begin, It end, Predicate func)
: current_{begin}, end_{end}, predicate_{func} {}
auto operator++() -> SplitWhenIterator& {
current_ = std::ranges::find_if(current_, end_, predicate_);
if (current_ != end_)
++current_;
return *this;
}
auto operator++(int) -> auto {
if constexpr (std::forward_iterator<It>) {
auto tmp = *this;
++*this;
return tmp;
} else
++*this;
}
auto operator*() const -> std::ranges::subrange<It> {
auto next = std::ranges::find_if(current_, end_, predicate_);
if (next != end_) return {current_, std::next(next)};
return {current_, next};
}
auto operator<=>(const SplitWhenIterator& rhs) const -> auto {
return std::tie(current_, end_) <=> std::tie(rhs.current_, rhs.end_);
}
auto operator==(const SplitWhenIterator& rhs) const -> bool {
return std::tie(current_, end_) == std::tie(rhs.current_, rhs.end_);
}
};
template <std::ranges::range R, std::predicate<std::ranges::range_value_t<R>> F>
SplitWhenView(R&&, F) -> SplitWhenView<std::views::all_t<R>, F>;
}
and, finally, my adaptor:
namespace so::views {
inline constexpr adaptor splitWhen =
[]<std::ranges::viewable_range Range, std::predicate<std::ranges::range_value_t<Range>> Predicate>(
Range&& range, Predicate&& predicate
) {
return SplitWhenView(std::forward<Range>(range), std::forward<Predicate>(predicate));
};
}
Views are required to be movable, which means that (among other things) they are required to be move-assignable.
Captureful lambdas are not move-assignable. So a class holding a captureful lambda is not move assignable either. So SplitWhenView<V, lambda>
is not actually a view (or a viewable_range
), hence the error.
(The iterator has similar issues too as it holds the predicate by value.)
This is why all the standard views hold function objects in a movable-box
, which implements move-assignment in terms of destroy/move-construct if the type is not otherwise move-assignable.