https://gcc.gnu.org/g:e4f550457b0b5c9c87356f6dfc60ae980e319585
commit r17-4556-ge4f550457b0b5c9c87356f6dfc60ae980e319585 Author: Patrick Palka <[email protected]> Date: Thu Sep 17 09:26:02 2026 +0200 libstdc++: Fix SFINAE with shared_ptr and invalid deleter [PR126120] 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]> Reviewed-by: Jonathan Wakely <[email protected]> Diff: --- libstdc++-v3/include/bits/shared_ptr.h | 22 ++++++++++- libstdc++-v3/include/bits/shared_ptr_base.h | 40 ++++++++++++------- .../testsuite/20_util/shared_ptr/cons/126120.cc | 45 ++++++++++++++++++++++ 3 files changed, 91 insertions(+), 16 deletions(-) diff --git a/libstdc++-v3/include/bits/shared_ptr.h b/libstdc++-v3/include/bits/shared_ptr.h index 8be9aace1711..99cedf12d436 100644 --- a/libstdc++-v3/include/bits/shared_ptr.h +++ b/libstdc++-v3/include/bits/shared_ptr.h @@ -102,6 +102,14 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION /// @cond undocumented + // Used as parameter type when constraining nullptr constructor + struct __shared_nullptr_ctor_tag + { + constexpr explicit + __shared_nullptr_ctor_tag(__shared_nullptr_ctor_tag*) + { } + }; + // Constraint for overloads taking non-array types. #if __cpp_concepts && __glibcxx_type_trait_variable_templates template<typename _Tp> @@ -166,6 +174,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 +225,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 +242,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(nullptr)) : __shared_ptr<_Tp>(__p, std::move(__d)) { } /** @@ -265,7 +282,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(nullptr)) : __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 c8342b7904a5..b67a697c6df6 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 000000000000..7a82358200e3 --- /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
