On Wed, Sep 3, 2025 at 11:37 PM Patrick Palka <[email protected]> wrote:
>
> On Wed, 3 Sep 2025, Tomasz Kamiński wrote:
>
> > This patch refactors ranges::_Partial to be implemented using
> _Bind_back_t.
> > This allows it to benefit from the changes in r16-3398-g250dd5b5604fbc,
> > specifically making the closure trivially copyable. Since _Bind_back_t
> > already provides an optimized implementation for a single bound argument,
> > specializations for _Partial with a single argument are now removed.
> >
> > We still preserve a specialization of _Partial for trivially
> copy-constructible
> > arguments that define only a const overload of operator(). To avoid
> > re-checking invocability constraints, this specialization calls the
> now-public,
> > unconstrained _Binder::_S_call static method instead of the constrained
> > _Binder::operator().
> >
> > The primary specialization of _Partial retains its operator(), which
> > uses a simpler __adaptor_invocable constraint that does not consider
> > member pointers, as they are not relevant here. This implementation also
> > calls _Binder::_S_call to avoid re-performing overload resolution and
> > invocability checks for _Binder::operator().
>
> Thanks for preserving/working around these _Partial optimizatinos! Note
> that
> the primary template (in C++20 mode) also just defines 3 operator()
> overloads instead of the full 8, sacrificing const&& invocability which
> presumably isn't useful in practice. (The _Pipe closure object does
> similar tricks.)
>
> The simpler __adaptor_invocable constraint was added not just because
> it's cheaper than is_invocable, but also because it made overload
> resolution failure diagnostics actually explain why the adaptor wasn't
> invocable.
Yes, the better quality diagnostic is was also an argument from preserving
the API, and using _Bind_back_t as storage.
> But now that we have builtins for is_invocable, and
> satisfaction failure replays __is_invocable failures (after Nathaniel's
> r16-2652-gbfb8615031a8c2), we might be able to get away with using
> is_invocable_v directly without sacrificing compile time performance
> and diagnostics.
I think a dedicated concept name is beneficial here.
> We could look into that as a follow-up.
>
> LGTM, the second patch in the series LGTM too.
>
> >
> > Finally, the _M_binder member (_Bind_back_t) is now marked
> > [[no_unique_address]]. This is beneficial as ranges::_Partial is used
> with
> > ranges::to, which commonly has zero or empty bound arguments (e.g.,
> stateless
> > allocators, comparators, or hash functions).
> >
> > libstdc++-v3/ChangeLog:
> >
> > * include/bits/binders.h (_Binder::_S_call): Make public.
> > * include/std/ranges (ranges::_Partial<_Adaptor, _Args...>):
> > Replace tuple<_Args...> with _Bind_back_t<_Adaptor, _Args...>.
> > (ranges::_Partial<_Adaptor, _Arg>): Remove.
> > ---
> > Tested on x86_64-linux locally. OK for trunk?
> >
> > libstdc++-v3/include/bits/binders.h | 10 +--
> > libstdc++-v3/include/std/ranges | 104 ++++------------------------
> > 2 files changed, 17 insertions(+), 97 deletions(-)
> >
> > diff --git a/libstdc++-v3/include/bits/binders.h
> b/libstdc++-v3/include/bits/binders.h
> > index 08a330da6fe..f274389e5b4 100644
> > --- a/libstdc++-v3/include/bits/binders.h
> > +++ b/libstdc++-v3/include/bits/binders.h
> > @@ -186,11 +186,6 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> > void operator()(_CallArgs&&...) const && = delete;
> > #endif
> >
> > - private:
> > - using _BoundArgsStorage
> > - // _BoundArgs are required to be move-constructible, so this is
> valid.
> > - =
> decltype(__make_bound_args<_BoundArgs...>(std::declval<_BoundArgs>()...));
> > -
> > template<typename _Tp, typename... _CallArgs>
> > static constexpr
> > decltype(auto)
> > @@ -214,6 +209,11 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> > std::forward<_CallArgs>(__call_args)...);
> > }
> >
> > + private:
> > + using _BoundArgsStorage
> > + // _BoundArgs are required to be move-constructible, so this is
> valid.
> > + =
> decltype(__make_bound_args<_BoundArgs...>(std::declval<_BoundArgs>()...));
> > +
> > [[no_unique_address]] _Fd _M_fd;
> > [[no_unique_address]] _BoundArgsStorage _M_bound_args;
> > };
> > diff --git a/libstdc++-v3/include/std/ranges
> b/libstdc++-v3/include/std/ranges
> > index 2970b2cbe00..98154a02e9e 100644
> > --- a/libstdc++-v3/include/std/ranges
> > +++ b/libstdc++-v3/include/std/ranges
> > @@ -51,6 +51,7 @@
> > #include <utility>
> > #include <variant>
> > #endif
> > +#include <bits/binders.h>
> > #include <bits/ranges_util.h>
> > #include <bits/refwrap.h>
> >
> > @@ -1047,14 +1048,15 @@ namespace views::__adaptor
> > template<typename _Adaptor, typename... _Args>
> > struct _Partial : _RangeAdaptorClosure<_Partial<_Adaptor, _Args...>>
> > {
> > - tuple<_Args...> _M_args;
> > + using _Binder = _Bind_back_t<_Adaptor, _Args...>;
> > + [[no_unique_address]] _Binder _M_binder;
> >
> > // First parameter is to ensure this constructor is never used
> > // instead of the copy/move constructor.
> > template<typename... _Ts>
> > constexpr
> > _Partial(int, _Ts&&... __args)
> > - : _M_args(std::forward<_Ts>(__args)...)
> > + : _M_binder(0, _Adaptor(), std::forward<_Ts>(__args)...)
> > { }
> >
> > // Invoke _Adaptor with arguments __r, _M_args... according to the
> > @@ -1065,75 +1067,21 @@ namespace views::__adaptor
> > constexpr auto
> > operator()(this _Self&& __self, _Range&& __r)
> > {
> > - auto __forwarder = [&__r] (auto&&... __args) {
> > - return _Adaptor{}(std::forward<_Range>(__r),
> > - std::forward<decltype(__args)>(__args)...);
> > - };
> > - return std::apply(__forwarder, __like_t<_Self,
> _Partial>(__self)._M_args);
> > + return _Binder::_S_call(__like_t<_Self,
> _Partial>(__self)._M_binder,
> > + std::forward<_Range>(__r));
> > }
> > #else
> > template<typename _Range>
> > requires __adaptor_invocable<_Adaptor, _Range, const _Args&...>
> > constexpr auto
> > operator()(_Range&& __r) const &
> > - {
> > - auto __forwarder = [&__r] (const auto&... __args) {
> > - return _Adaptor{}(std::forward<_Range>(__r), __args...);
> > - };
> > - return std::apply(__forwarder, _M_args);
> > - }
> > + { return _Binder::_S_call(_M_binder, std::forward<_Range>(__r)); }
> >
> > template<typename _Range>
> > requires __adaptor_invocable<_Adaptor, _Range, _Args...>
> > constexpr auto
> > operator()(_Range&& __r) &&
> > - {
> > - auto __forwarder = [&__r] (auto&... __args) {
> > - return _Adaptor{}(std::forward<_Range>(__r),
> std::move(__args)...);
> > - };
> > - return std::apply(__forwarder, _M_args);
> > - }
> > -
> > - template<typename _Range>
> > - constexpr auto
> > - operator()(_Range&& __r) const && = delete;
> > -#endif
> > - };
> > -
> > - // A lightweight specialization of the above primary template for
> > - // the common case where _Adaptor accepts a single extra argument.
> > - template<typename _Adaptor, typename _Arg>
> > - struct _Partial<_Adaptor, _Arg> :
> _RangeAdaptorClosure<_Partial<_Adaptor, _Arg>>
> > - {
> > - _Arg _M_arg;
> > -
> > - template<typename _Tp>
> > - constexpr
> > - _Partial(int, _Tp&& __arg)
> > - : _M_arg(std::forward<_Tp>(__arg))
> > - { }
> > -
> > -#if __cpp_explicit_this_parameter
> > - template<typename _Self, typename _Range>
> > - requires __adaptor_invocable<_Adaptor, _Range, __like_t<_Self,
> _Arg>>
> > - constexpr auto
> > - operator()(this _Self&& __self, _Range&& __r)
> > - {
> > - return _Adaptor{}(std::forward<_Range>(__r),
> > - __like_t<_Self, _Partial>(__self)._M_arg);
> > - }
> > -#else
> > - template<typename _Range>
> > - requires __adaptor_invocable<_Adaptor, _Range, const _Arg&>
> > - constexpr auto
> > - operator()(_Range&& __r) const &
> > - { return _Adaptor{}(std::forward<_Range>(__r), _M_arg); }
> > -
> > - template<typename _Range>
> > - requires __adaptor_invocable<_Adaptor, _Range, _Arg>
> > - constexpr auto
> > - operator()(_Range&& __r) &&
> > - { return _Adaptor{}(std::forward<_Range>(__r), std::move(_M_arg));
> }
> > + { return _Binder::_S_call(std::move(_M_binder),
> std::forward<_Range>(__r)); }
> >
> > template<typename _Range>
> > constexpr auto
> > @@ -1150,12 +1098,13 @@ namespace views::__adaptor
> > && (is_trivially_copy_constructible_v<_Args> && ...)
> > struct _Partial<_Adaptor, _Args...> :
> _RangeAdaptorClosure<_Partial<_Adaptor, _Args...>>
> > {
> > - tuple<_Args...> _M_args;
> > + using _Binder = _Bind_back_t<_Adaptor, _Args...>;
> > + [[no_unique_address]] _Binder _M_binder;
> >
> > template<typename... _Ts>
> > constexpr
> > _Partial(int, _Ts&&... __args)
> > - : _M_args(std::forward<_Ts>(__args)...)
> > + : _M_binder(0, _Adaptor(), std::forward<_Ts>(__args)...)
> > { }
> >
> > // Invoke _Adaptor with arguments __r, const _M_args&...
> regardless
> > @@ -1164,36 +1113,7 @@ namespace views::__adaptor
> > requires __adaptor_invocable<_Adaptor, _Range, const _Args&...>
> > constexpr auto
> > operator()(_Range&& __r) const
> > - {
> > - auto __forwarder = [&__r] (const auto&... __args) {
> > - return _Adaptor{}(std::forward<_Range>(__r), __args...);
> > - };
> > - return std::apply(__forwarder, _M_args);
> > - }
> > -
> > - static constexpr bool _S_has_simple_call_op = true;
> > - };
> > -
> > - // A lightweight specialization of the above template for the common
> case
> > - // where _Adaptor accepts a single extra argument.
> > - template<typename _Adaptor, typename _Arg>
> > - requires __adaptor_has_simple_extra_args<_Adaptor, _Arg>
> > - && is_trivially_copy_constructible_v<_Arg>
> > - struct _Partial<_Adaptor, _Arg> :
> _RangeAdaptorClosure<_Partial<_Adaptor, _Arg>>
> > - {
> > - _Arg _M_arg;
> > -
> > - template<typename _Tp>
> > - constexpr
> > - _Partial(int, _Tp&& __arg)
> > - : _M_arg(std::forward<_Tp>(__arg))
> > - { }
> > -
> > - template<typename _Range>
> > - requires __adaptor_invocable<_Adaptor, _Range, const _Arg&>
> > - constexpr auto
> > - operator()(_Range&& __r) const
> > - { return _Adaptor{}(std::forward<_Range>(__r), _M_arg); }
> > + { return _Binder::_S_call(_M_binder, std::forward<_Range>(__r)); }
> >
> > static constexpr bool _S_has_simple_call_op = true;
> > };
> > --
> > 2.51.0
> >
> >