[PATCH] libstdc++: Fix SFINAE with shared_ptr and invalid deleter [PR126120]

Tomasz Kaminski tkaminsk@redhat.com
Tue Aug 25 13:30:03 GMT 2026


On Tue, Aug 25, 2026 at 3:27 PM Patrick Palka <ppalka@redhat.com> wrote:

> On Tue, 25 Aug 2026, Patrick Palka wrote:
>
> > On Tue, 25 Aug 2026, Tomasz Kaminski wrote:
> >
> > >
> > >
> > > On Tue, Aug 25, 2026 at 6:11 AM Patrick Palka <ppalka@redhat.com>
> wrote:
> > >       Tested on x86_64-pc-linux-gnu, does this look OK for trunk/16?
> > >
> > >       -- >8 --
> > >
> > >       This implements the [util.smartptr.shared.const] constraints on
> the
> > >       constructors that take a deleter (which before C++20 were
> requirements
> > >       rather than constraints).
> > >
> > >               PR libstdc++/126120
> > >
> > >       libstdc++-v3/ChangeLog:
> > >
> > >               * include/bits/shared_ptr.h
> > >               (shared_ptr::shared_ptr(nullptr_t, ...)): Add
> _Constructible
> > >               constraint to verify __shared_ptr base initialization is
> > >               well-formed.
> > >               * include/bits/shared_ptr_base.h (_ValidDeleter): New
> __enable_if_t
> > >               constraint.
> > >               (__shared_ptr::__shared_ptr(..., _Deleter, ...)): Remove
> > >               static_assert checking deleter invocability.  Add
> _ValidDeleter
> > >               constraint.
> > >               * testsuite/20_util/shared_ptr/cons/126120.cc: New test.
> > >       ---
> > >        libstdc++-v3/include/bits/shared_ptr.h        |  5 +--
> > >        libstdc++-v3/include/bits/shared_ptr_base.h   | 29
> ++++++++---------
> > >        .../20_util/shared_ptr/cons/126120.cc         | 31
> +++++++++++++++++++
> > >        3 files changed, 49 insertions(+), 16 deletions(-)
> > >        create mode 100644
> libstdc++-v3/testsuite/20_util/shared_ptr/cons/126120.cc
> > >
> > >       diff --git a/libstdc++-v3/include/bits/shared_ptr.h
> b/libstdc++-v3/include/bits/shared_ptr.h
> > >       index 8be9aace1711..28b8a3ca8f29 100644
> > >       --- a/libstdc++-v3/include/bits/shared_ptr.h
> > >       +++ b/libstdc++-v3/include/bits/shared_ptr.h
> > >       @@ -225,7 +225,7 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> > >               *
> > >               *  The last owner will call __d(__p)
> > >               */
> > >       -      template<typename _Deleter>
> > >       +      template<typename _Deleter, typename =
> _Constructible<nullptr_t, _Deleter>>
> > >               shared_ptr(nullptr_t __p, _Deleter __d)
> > >                : __shared_ptr<_Tp>(__p, std::move(__d)) { }
> > >
> > > Could you add example from LWG4110 (
> https://cplusplus.github.io/LWG/lwg-active.html#4110),
> > > I think the resolution requires that we add a dummy parameter (with a
> default argument).
> > > which type is well-formed when _ValidDeleter is true.
> >
> > Done, like so?
> >
> > Subject: [PATCH] libstdc++: Fix SFINAE with shared_ptr and invalid
> deleter
> >  [PR126120]
> >
> > Tested on x86_64-pc-linux-gnu, does this look OK for trunk/16?
> >
> > Changes in v2:
> >   - Implement LWG 4110 proposed resolution, encoding the _ValidDeleter
> >     constraint as a function parameter instead of a template parameter.
> >   - Remove redundant is_move_constructible constraint in _ValidDeleter
> >     since the derived class's _Constructible constraint already checks
> >     it.
> >
> > -- >8 --
> >
> > This implements the [util.smartptr.shared.const] constraints on the
> > constructors that take a deleter (which before C++20 were requirements
> > rather than constraints).  In passing proactively implement the proposed
> > resolution of LWG 4110.
> >
> >       PR libstdc++/126120
> >
> > libstdc++-v3/ChangeLog:
> >
> >       * include/bits/shared_ptr.h
> >       (shared_ptr::shared_ptr(nullptr_t, ...)): Add _Constructible
> >       constraint to verify __shared_ptr base initialization is
> >       well-formed.  Add _ValidDeleter constraint.
> >       * include/bits/shared_ptr_base.h (_ValidDeleter): New __enable_if_t
> >       constraint.
> >       (__shared_ptr::__shared_ptr(Yp*, _Deleter, ...)): Remove
> >       static_assert checking deleter invocability.  Add _ValidDeleter
> >       constraint.
> >       * testsuite/20_util/shared_ptr/cons/126120.cc: New test.
> > ---
> >  libstdc++-v3/include/bits/shared_ptr.h        | 15 +++++--
> >  libstdc++-v3/include/bits/shared_ptr_base.h   | 21 ++++-----
> >  .../20_util/shared_ptr/cons/126120.cc         | 45 +++++++++++++++++++
> >  3 files changed, 65 insertions(+), 16 deletions(-)
> >  create mode 100644
> libstdc++-v3/testsuite/20_util/shared_ptr/cons/126120.cc
> >
> > diff --git a/libstdc++-v3/include/bits/shared_ptr.h
> b/libstdc++-v3/include/bits/shared_ptr.h
> > index 8be9aace1711..6b9acdcffa94 100644
> > --- a/libstdc++-v3/include/bits/shared_ptr.h
> > +++ b/libstdc++-v3/include/bits/shared_ptr.h
> > @@ -212,6 +212,10 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> >       shared_ptr(_Yp* __p, _Deleter __d)
> >          : __shared_ptr<_Tp>(__p, std::move(__d)) { }
> >
> > +      // _GLIBCXX_RESOLVE_LIB_DEFECTS
> > +      // 4110. shared_ptr(nullptr_t, Deleter) is overconstrained,
> breaking some
> > +      // sensible deleters
> > +
> >        /**
> >         *  @brief  Construct a %shared_ptr that owns a null pointer
> >         *          and the deleter @a __d.
> > @@ -225,8 +229,9 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> >         *
> >         *  The last owner will call __d(__p)
> >         */
> > -      template<typename _Deleter>
> > -     shared_ptr(nullptr_t __p, _Deleter __d)
> > +      template<typename _Deleter, typename = _Constructible<nullptr_t,
> _Deleter>>
> > +     shared_ptr(nullptr_t __p, _Deleter __d,
> > +                _ValidDeleter<_Deleter, nullptr_t> = {})
> >          : __shared_ptr<_Tp>(__p, std::move(__d)) { }
> >
> >        /**
> > @@ -264,8 +269,10 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> >         *
> >         *  The last owner will call __d(__p)
> >         */
> > -      template<typename _Deleter, typename _Alloc>
> > -     shared_ptr(nullptr_t __p, _Deleter __d, _Alloc __a)
> > +      template<typename _Deleter, typename _Alloc,
> > +            typename = _Constructible<nullptr_t, _Deleter, _Alloc>>
> > +     shared_ptr(nullptr_t __p, _Deleter __d, _Alloc __a,
> > +                _ValidDeleter<_Deleter, nullptr_t> = {})
> >       : __shared_ptr<_Tp>(__p, std::move(__d), std::move(__a)) { }
> >
> >        // Aliasing constructor
> > diff --git a/libstdc++-v3/include/bits/shared_ptr_base.h
> b/libstdc++-v3/include/bits/shared_ptr_base.h
> > index c8342b7904a5..7c1f965f0bb1 100644
> > --- a/libstdc++-v3/include/bits/shared_ptr_base.h
> > +++ b/libstdc++-v3/include/bits/shared_ptr_base.h
> > @@ -1469,6 +1469,10 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> >        { return static_cast<const __shared_ptr<_Tp, _Lp>*>(this)->get();
> }
> >      };
> >
> > +  // Constraint for well-formedness of deleter expression d(p):
> > +  template<typename _Deleter, typename _Up>
> > +    using _ValidDeleter = __enable_if_t<__is_invocable<_Deleter&,
> _Up&>::value, int>;
> > +
> >    template<typename _Tp, _Lock_policy _Lp>
> >      class __shared_ptr
> >      : public __shared_ptr_access<_Tp, _Lp>
> > @@ -1524,24 +1528,17 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> >         _M_enable_shared_from_this_with(__p);
> >       }
> >
> > -      template<typename _Yp, typename _Deleter, typename =
> _SafeConv<_Yp>>
> > +      template<typename _Yp, typename _Deleter, typename =
> _SafeConv<_Yp>,
> > +            typename = _ValidDeleter<_Deleter, _Yp*>>
> >       __shared_ptr(_Yp* __p, _Deleter __d)
> >       : _M_ptr(__p), _M_refcount(__p, std::move(__d))
> > -     {
> > -       static_assert(__is_invocable<_Deleter&, _Yp*&>::value,
> > -           "deleter expression d(p) is well-formed");
> > -       _M_enable_shared_from_this_with(__p);
> > -     }
> > +     { _M_enable_shared_from_this_with(__p); }
> >
> >        template<typename _Yp, typename _Deleter, typename _Alloc,
> > -            typename = _SafeConv<_Yp>>
> > +            typename = _SafeConv<_Yp>, typename =
> _ValidDeleter<_Deleter, _Yp*>>
> >       __shared_ptr(_Yp* __p, _Deleter __d, _Alloc __a)
> >       : _M_ptr(__p), _M_refcount(__p, std::move(__d), std::move(__a))
> > -     {
> > -       static_assert(__is_invocable<_Deleter&, _Yp*&>::value,
> > -           "deleter expression d(p) is well-formed");
> > -       _M_enable_shared_from_this_with(__p);
> > -     }
> > +     { _M_enable_shared_from_this_with(__p); }
> >
> >        template<typename _Deleter>
> >       __shared_ptr(nullptr_t __p, _Deleter __d)
> > diff --git a/libstdc++-v3/testsuite/20_util/shared_ptr/cons/126120.cc
> b/libstdc++-v3/testsuite/20_util/shared_ptr/cons/126120.cc
> > new file mode 100644
> > index 000000000000..0d3f84e2e779
> > --- /dev/null
> > +++ b/libstdc++-v3/testsuite/20_util/shared_ptr/cons/126120.cc
> > @@ -0,0 +1,45 @@
> > +// PR libstdc++/126120 - is_constructible_v with shared_ptr from an
> invalid deleter
> > +// { dg-do compile { target c++11 } }
> > +
> > +#include <memory>
> > +#include <type_traits>
> > +
> > +struct NonDeleter { };
> > +
> > +struct NonMoveableDeleter
> > +{
> > +  NonMoveableDeleter() = default;
> > +  NonMoveableDeleter(const NonMoveableDeleter&) = delete;
> > +  void operator()(int*) const;
> > +};
> > +
> > +using shared_ptr = std::shared_ptr<int>;
> > +using Alloc = std::allocator<int>;
> > +
> > +// 1. Pointer + Deleter
> > +static_assert( ! std::is_constructible<shared_ptr, int*,
> NonDeleter>::value, "");
> > +static_assert( ! std::is_constructible<shared_ptr, int*,
> NonMoveableDeleter>::value, "");
> > +
> > +// 2. Pointer + Deleter + Allocator
> > +static_assert( ! std::is_constructible<shared_ptr, int*, NonDeleter,
> Alloc>::value, "");
> > +static_assert( ! std::is_constructible<shared_ptr, int*,
> NonMoveableDeleter, Alloc>::value, "");
> > +
> > +// 3. nullptr_t + Deleter
> > +static_assert( ! std::is_constructible<shared_ptr, std::nullptr_t,
> NonDeleter>::value, "");
> > +static_assert( ! std::is_constructible<shared_ptr, std::nullptr_t,
> NonMoveableDeleter>::value, "");
> > +
> > +// 4. nullptr_t + Deleter + Allocator
> > +static_assert( ! std::is_constructible<shared_ptr, std::nullptr_t,
> NonDeleter, Alloc>::value, "");
> > +static_assert( ! std::is_constructible<shared_ptr, std::nullptr_t,
> NonMoveableDeleter, Alloc>::value, "");
> > +
> > +struct GenericDeleter {
> > +  template<typename T> void operator()(T*) const;
> > +};
>
> Whoops, in trying to make the LWG 4110 testcase C++11 compatible I
> made it no longer demonstrate the issue at all.  The call operator
> must have a deduced return type in order for the testcase to break
> without the proposed resolution.
>
You can use something like:
template<typename T>
struct non_nullptr
{
  static_assert(!is_same_v<T, nullptr>);
  using type = void;
};


and then return from operator() of deleted:
typename non_nullptr<T>::type


>
> > +
> > +void
> > +test01()
> > +{
> > +  // LWG 4110 - shared_ptr(nullptr_t, Deleter) is overconstrained,
> breaking some sensible deleters
> > +  shared_ptr p(new int, GenericDeleter{});
> > +  shared_ptr q(new int, GenericDeleter{}, Alloc{});
> > +}
> > --
> > 2.55.0.708.g2c3adbb2c4
> >
> >
> >
> > >
> > >
> > >
> > >       @@ -264,7 +264,8 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> > >               *
> > >               *  The last owner will call __d(__p)
> > >               */
> > >       -      template<typename _Deleter, typename _Alloc>
> > >       +      template<typename _Deleter, typename _Alloc,
> > >       +              typename = _Constructible<nullptr_t, _Deleter,
> _Alloc>>
> > >               shared_ptr(nullptr_t __p, _Deleter __d, _Alloc __a)
> > >              : __shared_ptr<_Tp>(__p, std::move(__d), std::move(__a))
> { }
> > >
> > >
> > >
> > >       diff --git a/libstdc++-v3/include/bits/shared_ptr_base.h
> b/libstdc++-v3/include/bits/shared_ptr_base.h
> > >       index c8342b7904a5..2eb3436183ee 100644
> > >       --- a/libstdc++-v3/include/bits/shared_ptr_base.h
> > >       +++ b/libstdc++-v3/include/bits/shared_ptr_base.h
> > >       @@ -1469,6 +1469,13 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> > >              { return static_cast<const __shared_ptr<_Tp,
> _Lp>*>(this)->get(); }
> > >            };
> > >
> > >       +  // Constraint for well-formedness of deleter expression d(p):
> > >       +  template<typename _Deleter, typename _Up>
> > >       +    using _ValidDeleter = __enable_if_t<__and_<
> > >       +      is_move_constructible<_Deleter>,
> > >       +      __is_invocable<_Deleter&, _Up&>
> > >       +      >::value>;
> > >       +
> > >          template<typename _Tp, _Lock_policy _Lp>
> > >            class __shared_ptr
> > >            : public __shared_ptr_access<_Tp, _Lp>
> > >       @@ -1524,31 +1531,25 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> > >                 _M_enable_shared_from_this_with(__p);
> > >               }
> > >
> > >       -      template<typename _Yp, typename _Deleter, typename =
> _SafeConv<_Yp>>
> > >       +      template<typename _Yp, typename _Deleter, typename =
> _SafeConv<_Yp>,
> > >       +              typename = _ValidDeleter<_Deleter, _Yp*>>
> > >               __shared_ptr(_Yp* __p, _Deleter __d)
> > >               : _M_ptr(__p), _M_refcount(__p, std::move(__d))
> > >       -       {
> > >       -         static_assert(__is_invocable<_Deleter&, _Yp*&>::value,
> > >       -             "deleter expression d(p) is well-formed");
> > >       -         _M_enable_shared_from_this_with(__p);
> > >       -       }
> > >       +       { _M_enable_shared_from_this_with(__p); }
> > >
> > >              template<typename _Yp, typename _Deleter, typename _Alloc,
> > >       -              typename = _SafeConv<_Yp>>
> > >       +              typename = _SafeConv<_Yp>, typename =
> _ValidDeleter<_Deleter, _Yp*>>
> > >               __shared_ptr(_Yp* __p, _Deleter __d, _Alloc __a)
> > >               : _M_ptr(__p), _M_refcount(__p, std::move(__d),
> std::move(__a))
> > >       -       {
> > >       -         static_assert(__is_invocable<_Deleter&, _Yp*&>::value,
> > >       -             "deleter expression d(p) is well-formed");
> > >       -         _M_enable_shared_from_this_with(__p);
> > >       -       }
> > >       +       { _M_enable_shared_from_this_with(__p); }
> > >
> > >       -      template<typename _Deleter>
> > >       +      template<typename _Deleter, typename =
> _ValidDeleter<_Deleter, nullptr_t>>
> > >               __shared_ptr(nullptr_t __p, _Deleter __d)
> > >               : _M_ptr(0), _M_refcount(__p, std::move(__d))
> > >               { }
> > >
> > >       -      template<typename _Deleter, typename _Alloc>
> > >       +      template<typename _Deleter, typename _Alloc,
> > >       +              typename = _ValidDeleter<_Deleter, nullptr_t>>
> > >                __shared_ptr(nullptr_t __p, _Deleter __d, _Alloc __a)
> > >               : _M_ptr(0), _M_refcount(__p, std::move(__d),
> std::move(__a))
> > >               { }
> > >       diff --git
> a/libstdc++-v3/testsuite/20_util/shared_ptr/cons/126120.cc
> b/libstdc++-v3/testsuite/20_util/shared_ptr/cons/126120.cc
> > >       new file mode 100644
> > >       index 000000000000..a5a29597e078
> > >       --- /dev/null
> > >       +++ b/libstdc++-v3/testsuite/20_util/shared_ptr/cons/126120.cc
> > >       @@ -0,0 +1,31 @@
> > >       +// PR libstdc++/126120 - is_constructible_v with shared_ptr
> from an invalid deleter
> > >       +// { dg-do compile { target c++11 } }
> > >       +
> > >       +#include <memory>
> > >       +#include <type_traits>
> > >       +
> > >       +struct NonDeleter { };
> > >       +
> > >       +struct NonMoveableDeleter {
> > >       +  NonMoveableDeleter() = default;
> > >       +  NonMoveableDeleter(const NonMoveableDeleter&) = delete;
> > >       +  void operator()(int* p) const { delete p; }
> > >       +};
> > >       +
> > >       +using Alloc = std::allocator<int>;
> > >       +
> > >       +// 1. Pointer + Deleter
> > >       +static_assert( ! std::is_constructible<std::shared_ptr<int>,
> int*, NonDeleter>::value, "");
> > >       +static_assert( ! std::is_constructible<std::shared_ptr<int>,
> int*, NonMoveableDeleter>::value, "");
> > >       +
> > >       +// 2. Pointer + Deleter + Allocator
> > >       +static_assert( ! std::is_constructible<std::shared_ptr<int>,
> int*, NonDeleter, Alloc>::value, "");
> > >       +static_assert( ! std::is_constructible<std::shared_ptr<int>,
> int*, NonMoveableDeleter, Alloc>::value, "");
> > >       +
> > >       +// 3. nullptr_t + Deleter
> > >       +static_assert( ! std::is_constructible<std::shared_ptr<int>,
> std::nullptr_t, NonDeleter>::value, "");
> > >       +static_assert( ! std::is_constructible<std::shared_ptr<int>,
> std::nullptr_t, NonMoveableDeleter>::value, "");
> > >       +
> > >       +// 4. nullptr_t + Deleter + Allocator
> > >       +static_assert( ! std::is_constructible<std::shared_ptr<int>,
> std::nullptr_t, NonDeleter, Alloc>::value, "");
> > >       +static_assert( ! std::is_constructible<std::shared_ptr<int>,
> std::nullptr_t, NonMoveableDeleter, Alloc>::value, "");
> > >       --
> > >       2.55.0.669.g593c42fe07
> > >
> > >
> > >
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://gcc.gnu.org/pipermail/libstdc++/attachments/20260825/d22c3fb1/attachment-0001.htm>


More information about the Libstdc++ mailing list