On Mon, Sep 21, 2026 at 4:13 PM Jonathan Wakely <[email protected]> wrote:

> On Thu, 17 Sep 2026 at 09:36 +0200, Tomasz Kamiński wrote:
> >From: Patrick Palka <[email protected]>
> >
> >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 which relaxes these constraints on the nullptr_t
> >overloads.
> >
> >       PR libstdc++/126120
> >
> >libstdc++-v3/ChangeLog:
> >
> >       * include/bits/shared_ptr.h (std::__shared_nullptr_ctor_tag):
> >       Define.
> >       (shared_ptr::shared_ptr(nullptr_t, ...)): Constrain _Deleter
> >       via _NullptrDeleter parameter.
> >       * include/bits/shared_ptr_base.h (__shared_ptr::_ValidDeleter):
> >       New __enable_if_t constraint.
> >       (__shared_ptr::__shared_ptr(Yp*, _Deleter, ...)): Add _ValidDeleter
> >       constraint. Move static_assert checking deleter invocability
> >       to corresponding reset functions.
> >       (__shared_ptr::reset): Add static_assert checks for deleter
> >       invocability.
> >       * testsuite/20_util/shared_ptr/cons/126120.cc: New test.
> >
> >Co-authored-by: Tomasz Kamiński <[email protected]>
> >---
> >v4 appies the review suggestion:
> >
> >1. I have moved _ValidDeleter into __shared_ptr. However, it was also
> >used in shared_ptr(nullptr_t) constructors, and instead of making it
>
> Ah yes, that's probably why it was at namespace-scope.
>
> >private, I have declared _NullptrDelete helper, separating the case
> >when non-void result of enable_if is needed.
>
> Nice.
>
> >The __shared_nullptr_ctor_tag (I proud of this name instead of
> >__shared_ptr_nullptr_ctor_tag, so changing it is not negotiable)
>
> I like the name.
>
> >is declared in global namespace, to avoid instantiating separate type
> >for each shared_ptr<T> specializations. It default constructor is
> >explicit, so shared_ptr<in>(nullptr, deleter, {}) remains ill-formed.**
> >(You couldn't deduce Alloc from braces, so other constructor was never
> >an candidate).
>
> I agree that it doesn't change whether any code is ill-formed or not,
> but it does affect diagnostic quality.
>
> Previously code which tried to default-list-initialize an allocator
> might have tried:
>
>    shared_ptr<int> p(nullptr, Deleter{}, {});
>
> and you get a "no matching function" error that showed all the
> candidate constructors. With this patch (thanks to my least favourite
> rule in C++ about explicit default constructors being overload
> resolution candidates) you would get:
>
> sp.cc:8:49: error: converting to
> 'std::shared_ptr<int>::_NullptrDeleter<Deleter>' {aka 'std::enable_if<true,
> std::__shared_nullptr_ctor_tag>::type'} from initializer list would use
> explicit constructor 'constexpr
> std::__shared_nullptr_ctor_tag::__shared_nullptr_ctor_tag()'
>      8 |   std::shared_ptr<int> sp(nullptr, Deleter{}, {});
>        |                                                 ^
>
> This is pretty confusing. What are these types, and what has an
> explicit constructor got to do with anything? And it doesn't show the
> other candidates.
>
> Could you make this type has a non-default constructor, like
> std::nullopt_t?
>
>      explicit __shared_nullptr_ctor_tag(int) { }
>
> That way we still get the full list of candidate constructors for the
> attempt to construct a shared_ptr with {}.
>
> What do you think?
>
I was thinking about that, but then we get confusing messages if someone's
does  std::shared_ptr<int> sp(nullptr, Deleter{}, {id});, where the id is
the personality
or bucket of the allocator. So wasn't convinced that this is any better.
But, I see
tour point that {} braces are significantly more likely.

And I didn't want to add yet another type for the parameter type. Maybe
__shared_nullptr_ctor_tag* would be better, and will change default
argument to
 __shared_nullptr_ctor_tag(nullptr)? That will leave us with confusing
messages only for zero or nullptr. Let me know if you agree, and want me
to pepare next revison or just make the change and push it?

>
> >2. Removed _Constructible requirements on shared_ptr(nullptr_t, ...),
> >as the corresponding __shared_ptr constructor have no constraints.
> >I think it was leftover from previous version.
> >
> >3. Moved static_assert from constructor to reset method.
>
> Great, thanks.
>
> >Testing on x86_64-linux. *shared_ptr* test passed in all standard modes,
> >debug, assertions. OK for trunk when all test passes?
> >
> > libstdc++-v3/include/bits/shared_ptr.h        | 20 ++++++++-
> > libstdc++-v3/include/bits/shared_ptr_base.h   | 40 +++++++++++------
> > .../20_util/shared_ptr/cons/126120.cc         | 45 +++++++++++++++++++
> > 3 files changed, 89 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 8be9aace171..058d8456a43 100644
> >--- a/libstdc++-v3/include/bits/shared_ptr.h
> >+++ b/libstdc++-v3/include/bits/shared_ptr.h
> >@@ -102,6 +102,12 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> >
> >   /// @cond undocumented
> >
> >+  // Used as parameter type when constraining nullptr constructor
> >+  struct __shared_nullptr_ctor_tag
> >+  {
> >+    explicit __shared_nullptr_ctor_tag() = default;
> >+  };
> >+
> >   // Constraint for overloads taking non-array types.
> > #if __cpp_concepts && __glibcxx_type_trait_variable_templates
> >   template<typename _Tp>
> >@@ -166,6 +172,11 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> >         is_assignable<__shared_ptr<_Tp>&, _Arg>::value, shared_ptr&
> >       >::type;
> >
> >+      template<typename _Deleter>
> >+      using _NullptrDeleter = __enable_if_t<
> >+        __is_invocable<_Deleter&, nullptr_t&>::value,
> >+        __shared_nullptr_ctor_tag>;
> >+
> >     public:
> >
> >       /// The type pointed to by the stored pointer, remove_extent_t<_Tp>
> >@@ -212,6 +223,9 @@ _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...
> >+
> >       /**
> >        *  @brief  Construct a %shared_ptr that owns a null pointer
> >        *          and the deleter @a __d.
> >@@ -226,7 +240,8 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> >        *  The last owner will call __d(__p)
> >        */
> >       template<typename _Deleter>
> >-      shared_ptr(nullptr_t __p, _Deleter __d)
> >+      shared_ptr(nullptr_t __p, _Deleter __d,
> >+                 _NullptrDeleter<_Deleter> = __shared_nullptr_ctor_tag())
> >         : __shared_ptr<_Tp>(__p, std::move(__d)) { }
> >
> >       /**
> >@@ -265,7 +280,8 @@ _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)
> >+      shared_ptr(nullptr_t __p, _Deleter __d, _Alloc __a,
> >+                 _NullptrDeleter<_Deleter> = __shared_nullptr_ctor_tag())
> >       : __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 c8342b7904a..b67a697c6df 100644
> >--- a/libstdc++-v3/include/bits/shared_ptr_base.h
> >+++ b/libstdc++-v3/include/bits/shared_ptr_base.h
> >@@ -1504,6 +1504,11 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> >       template<typename _Yp, typename _Del>
> >       using _UniqAssignable = _UniqCompatible<_Yp, _Del, __shared_ptr&>;
> >
> >+      // Constraint for well-formedness of deleter expression d(p):
> >+      template<typename _Deleter, typename _Up>
> >+      using _ValidDeleter = __enable_if_t<
> >+        __is_invocable<_Deleter&, _Up&>::value>;
> >+
> >     public:
> >
> > #if __cplusplus > 201402L
> >@@ -1524,24 +1529,21 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> >         _M_enable_shared_from_this_with(__p);
> >       }
> >
> >-      template<typename _Yp, typename _Deleter, typename =
> _SafeConv<_Yp>>
> >+      // _GLIBCXX_RESOLVE_LIB_DEFECTS
> >+      // 4110. shared_ptr(nullptr_t, Deleter) is overconstrained,
> breaking some
> >+      //       sensible deleters
> >+
> >+      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)
> >@@ -1704,12 +1706,22 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
> >       template<typename _Yp, typename _Deleter>
> >       _SafeConv<_Yp>
> >       reset(_Yp* __p, _Deleter __d)
> >-      { __shared_ptr(__p, std::move(__d)).swap(*this); }
> >+      {
> >+        static_assert(__is_invocable<_Deleter&, _Yp*&>::value,
> >+          "deleter expression d(p) is well-formed");
> >+
> >+        __shared_ptr(__p, std::move(__d)).swap(*this);
> >+      }
> >
> >       template<typename _Yp, typename _Deleter, typename _Alloc>
> >       _SafeConv<_Yp>
> >       reset(_Yp* __p, _Deleter __d, _Alloc __a)
> >-        { __shared_ptr(__p, std::move(__d), std::move(__a)).swap(*this);
> }
> >+      {
> >+        static_assert(__is_invocable<_Deleter&, _Yp*&>::value,
> >+          "deleter expression d(p) is well-formed");
> >+
> >+        __shared_ptr(__p, std::move(__d), std::move(__a)).swap(*this);
> >+      }
> >
> >       /// Return the stored pointer.
> >       element_type*
> >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 00000000000..7a82358200e
> >--- /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* p) const { delete p; }
> >+};
> >+
> >+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, "");
> >+
> >+#pragma GCC diagnostic push
> >+#pragma GCC diagnostic ignored "-Wc++20-extensions" // lambda template
> >+void
> >+test_lwg4110()
> >+{
> >+  // LWG 4110 - shared_ptr(nullptr_t, Deleter) is overconstrained,
> breaking some sensible deleters
> >+  auto deleter = []<typename T>(T pointer) { delete pointer; };
> >+  shared_ptr p(new int, deleter);
> >+  shared_ptr q(new int, deleter, Alloc{});
> >+}
> >+#pragma GCC diagnostic pop
> >--
> >2.55.0
> >
> >
>
>

Reply via email to