[PATCH v4] libstdc++: Fix SFINAE with shared_ptr and invalid deleter [PR126120]
Jonathan Wakely
jwakely@redhat.com
Mon Sep 21 14:13:46 GMT 2026
On Thu, 17 Sep 2026 at 09:36 +0200, Tomasz Kamiński wrote:
>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
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?
>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
>
>
More information about the Libstdc++
mailing list