C++20: implementing pairwise_filter_view

109 Views Asked by At

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?
0

There are 0 best solutions below