On Mon, 21 Sept 2026 at 15:28, Tomasz Kaminski <[email protected]> wrote:
>
>
>
> 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

Yes I agree.

> to pepare next revison or just make the change and push it?

OK to push it with that change, thanks.

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