P2387R3: Pipe support for user-defined range adaptors

P2387R3 is a proposal for C++23 that allows user to define their range adaptors that will inter-operate with std adaptors.

Range Adaptor Closure Object

The range adaptor closure object is a function object that accepts a range argument. For a range adaptor closure object C and an expression R such that decltype((R)) models range, R | C is equivalent to C(R). Given an additional range adaptor closure object D, the expression C | D is well-formed and produces a range adaptor closure object E, which is a perfect forwarding call wrapper. Let c be an object of type decay_t<decltype((C)) and d be decay_t<decltype((D)), the closure object E will call d(c(arg)), where arg is the argument used in a function call expression of E.

The ranges::range_adaptor_closure CRTP-based class template is an interface that range adaptor closure objects will have to inherit. The derived class C needs to provide a call operator C(R), and ranges::range_adaptor_closure<C> will provide an operator| that handles both R | C and C | D. The following code snippet is an implementation of ranges::range_adaptor_closure:

template <typename T> struct range_adaptor_closure;

// Wraps a function object and makes it into a `range_adaptor_closure`
template <typename Fn>
struct range_adaptor_closure_t
    : Fn,
      range_adaptor_closure<range_adaptor_closure_t<Fn>> {
  constexpr explicit range_adaptor_closure_t(Fn &&f) : Fn(std::move(f)) {}
};

template <typename T>
concept RangeAdaptorClosure = std::derived_from<
    std::remove_cvref_t<T>, range_adaptor_closure<std::remove_cvref_t<T>>>;

template <typename T> struct range_adaptor_closure {
  // `R | C` is equivalent to `C(R)`
  template <std::ranges::viewable_range View, RangeAdaptorClosure Closure>
    requires std::same_as<T, std::remove_cvref_t<Closure>> &&
             std::invocable<Closure, View>
  [[nodiscard]] friend constexpr decltype(auto) operator|(
      View &&view, Closure &&closure
  ) noexcept(std::is_nothrow_invocable_v<Closure, View>) {
    return std::invoke(
        std::forward<Closure>(closure), std::forward<View>(view)
    );
  }

  // `C | D` is well-formed and produces a `range_adaptor_closure` object `E`
  template <RangeAdaptorClosure Closure, RangeAdaptorClosure OtherClosure>
    requires std::same_as<T, std::remove_cvref_t<Closure>> &&
             std::constructible_from<std::decay_t<Closure>, Closure> &&
             std::constructible_from<std::decay_t<OtherClosure>, OtherClosure>
  [[nodiscard]] friend constexpr auto operator|(
      Closure &&__c1, OtherClosure &&__c2
  ) noexcept(std::is_nothrow_constructible_v<std::decay_t<Closure>, Closure> && std::is_nothrow_constructible_v<std::decay_t<OtherClosure>, OtherClosure>) {
    return range_adaptor_closure_t(std::__compose(
        std::forward<OtherClosure>(__c2), std::forward<Closure>(__c1)
    ));
  }
};

Range Adaptor Object

Range adaptor object is a customization point object that accepts viewable_range as its first argument and returns a view. For a range adaptor A and an expression R such that decltype((R)) models viewable_range, A(R, args...), A(args...)(R), and R | A(args...) are equivalent, where A(args...) produces a range adaptor closure object.

As described in the proposal, different std implementations have various strategies to support these operations. For example, the range adaptor objects can define an explicit operator(args...) that returns a range adaptor closure object. The following code snippet is an implementation of views::take that handles the case when the first argument is an empty_view:

template <typename T> inline constexpr bool is_empty_view = false;
template <typename T>
inline constexpr bool is_empty_view<std::ranges::empty_view<T>> = true;

struct take_fn {
  // `A(R, args...)`
  template <
      typename Range,
      std::convertible_to<std::ranges::range_difference_t<Range>> N>
    requires is_empty_view<Range>
  [[nodiscard]] static constexpr auto operator()(Range &&range, N &&) noexcept(
      noexcept(auto(std::forward<Range>(range)))
  ) -> decltype(auto(std::forward<Range>(range))) {
    return auto(std::forward<Range>(range));
  }

  // `A(args...)(R)` and `R | A(args...)`
  template <typename N>
    requires std::constructible_from<std::decay_t<N>, N>
  [[nodiscard]] constexpr auto operator()(N &&n) const
      noexcept(std::is_nothrow_constructible_v<std::decay_t<N>, N>) {
    // `A(args...)` returns a range adaptor closure object
    return range_adaptor_closure_t(std::bind_back(*this, std::forward<N>(n)));
  }
};

inline constexpr auto take = take_fn{};