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