Search code examples
c++stlc++20range-v3

Range trim view implementation does not work with reverse view


I wrote a C++20 range view (not a range-v3 view) called trim that, given a range and a unary predicate, returns a new range without the front and back elements that satisfy the predicate. (The range-v3 library has such a view, but it's missing from C++20.)

Here is the implementation (this may not be optimal, but there isn't a lot of documentation on the ranges library, so this is what I could come up with given the limited resources):

namespace rg = std::ranges;

// -------- trim ----------

template<rg::input_range R, typename P> requires rg::view<R>
class trim_view : public rg::view_interface<trim_view<R, P>>
{
private:
    R base_ {};
    P pred_;
    mutable rg::iterator_t<R> iter_ {std::begin(base_)};
    mutable rg::iterator_t<R> end_  {std::end(base_)};
public:
    trim_view() = default;

    constexpr trim_view(R base, P pred)
        : base_(std::move(base)), pred_(std::move(pred)), iter_(std::begin(base_)), end_(std::end(base_))
    {}

    constexpr R base() const &
    {return base_;}
    constexpr R base() && 
    {return std::move(base_);}

    constexpr auto begin()
    {
        while(iter_ != std::end(base_) && pred_(*iter_)) {iter_ = std::next(iter_);} 
        while(end_ != iter_ && pred_(*std::prev(end_))) {end_ = std::prev(end_);}
        return iter_;
    }
    constexpr auto begin() const requires rg::range<const R>
    {
        while(iter_ != std::end(base_) && pred_(*iter_)) {iter_ = std::next(iter_);} 
        while(end_ != iter_ && pred_(*std::prev(end_))) {end_ = std::prev(end_);}
        return iter_;
    }
    constexpr auto begin() requires rg::random_access_range<R> && rg::sized_range<R>
    {
        while(iter_ != std::end(base_) && pred_(*iter_)) {iter_ = std::next(iter_);} 
        while(end_ != iter_ && pred_(*std::prev(end_))) {end_ = std::prev(end_);}
        return iter_;
    }
    constexpr auto begin() const requires rg::random_access_range<const R> && rg::sized_range<const R>
    {
        while(iter_ != std::end(base_) && pred_(*iter_)) {iter_ = std::next(iter_);} 
        while(end_ != iter_ && pred_(*std::prev(end_))) {end_ = std::prev(end_);}
        return iter_;
    }

    constexpr auto end()
    { return end_ ; }
    constexpr auto end() const requires rg::range<const R>
    { return end_ ; }
    constexpr auto end() requires rg::random_access_range<R> && rg::sized_range<R>
    { return end_ ; }
    constexpr auto end() const requires rg::random_access_range<const R> && rg::sized_range<const R>
    { return end_ ; }

    constexpr auto size() requires rg::sized_range<R>
    { return std::distance(iter_, end_); }    
    constexpr auto size() const requires rg::sized_range<const R>
    { return std::distance(iter_, end_); }
};

template<class R, typename P>
trim_view(R&& base, P pred)
    -> trim_view<rg::views::all_t<R>, P>;

namespace details
{
    template <typename P>
    struct trim_view_range_adaptor_closure
    {
        P pred_;
        constexpr trim_view_range_adaptor_closure(P pred)
            : pred_(pred)
        {}

        template <rg::viewable_range R>
        constexpr auto operator()(R && r) const
        {
            return trim_view(std::forward<R>(r), pred_);
        }
    } ;

    struct trim_view_range_adaptor
    {
        template<rg::viewable_range R, typename P>
        constexpr auto operator () (R && r, P pred)
        {
            return trim_view( std::forward<R>(r), pred ) ;
        }

        template <typename P>
        constexpr auto operator () (P pred)
        {
            return trim_view_range_adaptor_closure(pred);
        }   
    };

    template <rg::viewable_range R, typename P>
    constexpr auto operator | (R&& r, trim_view_range_adaptor_closure<P> const & a)
    {
        return a(std::forward<R>(r)) ;
    }
}

namespace views
{
    inline static details::trim_view_range_adaptor trim;
}

It works fine. I wrote some tests to make sure it's OK.

template <typename P>
void are_equal(std::vector<int> const & input, std::vector<int> const & output, P&& pred)
{
    std::size_t index = 0;
    for(auto  i : input | views::trim(std::forward<P>(pred)))
    {
        assert(i == output[index]);
        index++;
    }
    assert(index == output.size());
}

int main()
{
    auto is_odd = [](const int x){return x%2==1;};

    are_equal({}, {}, is_odd);
    are_equal({1}, {}, is_odd);    
    are_equal({1,3,5}, {}, is_odd);
    are_equal({2}, {2}, is_odd);
    are_equal({2,4}, {2,4}, is_odd);
    are_equal({2,3,4}, {2,3,4}, is_odd);
    are_equal({1,2,3,4}, {2,3,4}, is_odd);
    are_equal({1,1,2,3,4}, {2,3,4}, is_odd);
    are_equal({2,3,4,5}, {2,3,4}, is_odd);
    are_equal({2,3,4,5,5}, {2,3,4}, is_odd);
    are_equal({1,2,3,4,5}, {2,3,4}, is_odd);
    are_equal({1,1,2,3,4,5,5}, {2,3,4}, is_odd);
}

The problem is, when I apply the views::reverse view after trimming then it no longer works properly.

template <typename P>
void are_equal_reverse2(std::vector<int> const & input, std::vector<int> const & output, P&& pred)
{
    std::size_t index = 0;
    for(auto  i : input | views::trim(std::forward<P>(pred)) | rg::views::reverse)
    {
        assert(i == output[index]);
        index++;
    }
    assert(index == output.size());
}

int main()
{
    auto is_odd = [](const int x){return x%2==1;};

    // OK
    are_equal_reverse2({}, {}, is_odd);
    are_equal_reverse2({1}, {}, is_odd);
    are_equal_reverse2({1,3,5}, {}, is_odd);
    are_equal_reverse2({2}, {2}, is_odd);
    are_equal_reverse2({2,4}, {4,2}, is_odd);
    are_equal_reverse2({2,3,4}, {4,3,2}, is_odd);
    are_equal_reverse2({1,2,3,4}, {4,3,2}, is_odd);
    are_equal_reverse2({1,1,2,3,4}, {4,3,2}, is_odd);

    // fail
    are_equal_reverse2({2,3,4,5}, {4,3,2}, is_odd);
    are_equal_reverse2({2,3,4,5,5}, {4,3,2}, is_odd);
    are_equal_reverse2({1,2,3,4,5}, {4,3,2}, is_odd);
    are_equal_reverse2({1,1,2,3,4,5,5}, {4,3,2}, is_odd);
}

The range {2,3,4,5} becomes {2,3,4}. With reverse applied, it should become {4,3,2}. However, the result is actually {5,4,3,2}.

I expect that views::reverse applies std::make_reverse_iterator() on the begin and end iterators of the trim view. That should perform the following transformation:

trim_view        reverse_view (expected)      reverse_view (actual)
--------------------------------------------------------------------
2 3 4 5 _         _ 2 3 4 5                   _ 2 3 4 5
^     ^           ^     ^                     ^       ^
|     |      =>   |     |                     |       |
|     end_        rend  |                     rend    |
iter_                   rbegin                        rbegin

I am not sure what I am missing here. Any help is appreciated.

Here is the link to a working sample: https://wandbox.org/permlink/4iFNsqiz9Y9Bfm64


Solution

  • First of all, let's start with this:

    constexpr auto begin()
    {
        while(iter_ != std::end(base_) && pred_(*iter_)) {iter_ = std::next(iter_);} 
        while(end_ != iter_ && pred_(*std::prev(end_))) {end_ = std::prev(end_);}
        return iter_;
    }
    constexpr auto begin() const requires rg::range<const R>
    {
        while(iter_ != std::end(base_) && pred_(*iter_)) {iter_ = std::next(iter_);} 
        while(end_ != iter_ && pred_(*std::prev(end_))) {end_ = std::prev(end_);}
        return iter_;
    }
    constexpr auto begin() requires rg::random_access_range<R> && rg::sized_range<R>
    {
        while(iter_ != std::end(base_) && pred_(*iter_)) {iter_ = std::next(iter_);} 
        while(end_ != iter_ && pred_(*std::prev(end_))) {end_ = std::prev(end_);}
        return iter_;
    }
    constexpr auto begin() const requires rg::random_access_range<const R> && rg::sized_range<const R>
    {
        while(iter_ != std::end(base_) && pred_(*iter_)) {iter_ = std::next(iter_);} 
        while(end_ != iter_ && pred_(*std::prev(end_))) {end_ = std::prev(end_);}
        return iter_;
    }
    
    constexpr auto end()
    { return end_ ; }
    constexpr auto end() const requires rg::range<const R>
    { return end_ ; }
    constexpr auto end() requires rg::random_access_range<R> && rg::sized_range<R>
    { return end_ ; }
    constexpr auto end() const requires rg::random_access_range<const R> && rg::sized_range<const R>
    { return end_ ; }
    

    All of these overloads do the exact same thing, so let's just reduce down to two:

    constexpr auto begin() const
    {
        while(iter_ != std::end(base_) && pred_(*iter_)) {iter_ = std::next(iter_);} 
        while(end_ != iter_ && pred_(*std::prev(end_))) {end_ = std::prev(end_);}
        return iter_;
    }
    
    constexpr auto end() const
    { return end_ ; }
    

    Alright now. What's going on here? begin() will adjust both iter_ and end_ to be trimmed, and end() just returns end_.

    That's fine and all if you do this:

    auto trimmed = some_range | trim(some_pred);
    auto b = trimmed.begin();
    auto e = trimmed.end();
    

    But what happens if you do this:

    auto e = trimmed.end();
    auto b = trimmed.begin();
    

    end in this case will be some_range.end(), it won't be the correct end iterator for this range! You need to ensure that begin() and end() don't have any ordering dependencies between them - they always have to return the correct value.


    Also, trim(p) can be reduced in its entirety to:

    template <rg::viewable_range R, typename P>
    constexpr auto operator ()(R && r, P pred)
    {
        auto negated = std::not_fn(pred);
        auto f = rg::find_if(r, negated);
        auto l = rg::find_if(r | std::views::reverse, negated).base();
        return rg::subrange(f, l);
    }