[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