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

Tomasz Kamiński tkaminsk@redhat.com
Thu Sep 17 07:36:29 GMT 2026


From: Patrick Palka <ppalka@redhat.com>

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 <tkaminsk@redhat.com>
---
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
private, I have declared _NullptrDelete helper, separating the case
when non-void result of enable_if is needed.
The __shared_nullptr_ctor_tag (I proud of this name instead of 
__shared_ptr_nullptr_ctor_tag, so changing it is not negotiable)
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).

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.

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



More information about the Libstdc++ mailing list