Search code examples
c++std-rangesc++23

User-defined pipeable view adaptor fails when lambda-capture is not empty


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));
            };
}

Solution

  • 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.