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