I had the need to implement a pairwise_filter_view
, e.g., skipping pairs of contiguous elements that don't satisfy a predicate. Example, if I want to skip all the ocurrences of "yy" in "xyyxyxyyxy", the result would be: "xxyxxy".
My take:
#include <iostream>
#include <ranges>
namespace rng = std::ranges;
namespace vw = std::views;
template<rng::forward_range R,
std::predicate<rng::range_value_t<R>, rng::range_value_t<R>> P>
requires rng::view<R>
class pairwise_filter_view
: public rng::view_interface<pairwise_filter_view<R, P>>
{
struct _sentinel_t {};
class _iterator_t
{
rng::iterator_t<R> _it{};
rng::iterator_t<R> _jt{}; // lookahead 1
rng::sentinel_t<R> _end{};
P _pred{};
public:
_iterator_t() = default;
_iterator_t(R r, P p) : _it{r.begin()}, _jt{r.begin()},
_end{r.end()}, _pred{p}
{ _filter(); }
rng::range_reference_t<R> operator*() const { return *_it; }
_iterator_t& operator++() { _it = _jt; _filter(); return *this; }
_iterator_t& operator++(int) { auto r{*this}; ++*this; return r; }
friend bool operator!=(_iterator_t it, _sentinel_t end)
{ return it._it != it._end; }
private:
void _filter() // precond: _it == _jt
{
if (_jt != _end)
while (++_jt != _end && !_pred(*_it, *_jt))
if ((_it = ++_jt) == _end)
return;
}
};
R _base{};
P _pred{};
public:
pairwise_filter_view() = default;
pairwise_filter_view(R r, P p) : _base{r}, _pred{p} {}
R base() const { return _base; }
auto begin() const { return _iterator_t{_base, _pred}; }
auto end() const { return _sentinel_t{}; }
};
namespace _impl {
template<class P>
struct pairwise_filter_closure { P p; };
template<rng::forward_range R, typename P>
requires rng::view<R>
auto operator|(R r, pairwise_filter_closure<P> c)
{ return pairwise_filter_view{r, c.p}; }
}
template<class P>
inline auto pairwise_filter(P p) { return _impl::pairwise_filter_closure<P>{p}; }
int main()
{
std::string str = "addex";
// Prints adx
for (char c : str | vw::all | pairwise_filter([](char a, char b) { return !(a == 'd' && b == 'e'); }))
std::cout << c;
std::cout << '\n';
return 0;
}
So far, the view does the right thing, but notice that I have to use views::all
to turn the string into a view first (I added a requires rng::view<R>
restriction): I don't want the view to store any container, because trivially-copyable type semantics are easier to reason about.
However, I want to allow the user to simply write str | pairwise_filter(....)
, and store a view of str
within the view transparently, as far as the range is neither an anonymous "container" or a view (storing a view of a view feels unnecesary).
The strategy would be:
- If the range is already a view, ok,
R =
such type. - Otherwise, if it's a non-viewable or not a forward range => error.
- Otherwise, if it's a forward range but not a view,
R = views::all_t<R>
But, regarding the last point, I have to be careful about how the range has been received as argument (of the constructor, pipe, template parameter, ...):
- If I got an
A&&
=> error (to avoid views over dangling pointers) - If I got an
A const&
=>R = views::all_t<A const>
- If I got an
A&
=>R = views::all_t<A>
I can go the old boilerplate way of using type_traits
, partial specializations, overloading the constructor and/or overloading the pipe operator, etc, but I want to know what would be the most comfortable way to do it using modern features.
The most "straightforward" way feels like using deduction guides: it catches in a single place both direct usages of the class, but also indirect constructions through the pipe (by forwarding the argument instead of copying): so both entry points to the class are covered.
However, I'm unsure about the semantics for deduction guides:
template<class R, class P>
// shall I "repeat" the template parameters exactly as they appear in the class?
pairwise_filter_view(R&& r, P p)
// There's no constructor with that syntax. Is that allowed?
-> ????
// ^ Can I use "if constexpr" syntax here or do I need to provide several
// specializations of the deduction guide?
//
// If R&& is the "normal case" (it's already a view), do I need to have a deduction
// guide for it as base case, or "omitted possibilities" are automatically handed by the
// compiler somehow already?