[1/3] libstdc++: Replace uses of EBO with [[no_unique_address]]

Message ID 20260505150351.209503-1-jwakely@redhat.com
State New
Headers
Series [1/3] libstdc++: Replace uses of EBO with [[no_unique_address]] |

Commit Message

Jonathan Wakely May 5, 2026, 3:03 p.m. UTC
  Clang 9 added support for [[__no_unique_address__]] and we don't support
Intel icc any longer, so we can remove the code in <tuple> that works
around the absence of that attribute. We can also address a FIXME in
<bits/shared_ptr_base.h> and replace uses of EBO with the attribute.

libstdc++-v3/ChangeLog:

	* include/bits/shared_ptr_base.h (_Sp_ebo_helper): Simplify by
	using [[__no_unique_address__]] instead of EBO. Use the
	attribute unconditionally for the unstable ABI.
	(_Sp_counted_deleter::_Impl): Adjust uses of _Sp_ebo_helper.
	(_Sp_counted_ptr_inplace::_Impl): Likewise.
	* include/std/tuple (_Head_base): Remove implementation for
	compilers that don't support [[__no_unique_address__]]. Use the
	attribute unconditionally for the unstable ABI.
---

Tested x86_64-linux.

 libstdc++-v3/include/bits/shared_ptr_base.h | 65 +++++++++----------
 libstdc++-v3/include/std/tuple              | 69 ++++-----------------
 2 files changed, 43 insertions(+), 91 deletions(-)
  

Comments

Jonathan Wakely May 5, 2026, 5:56 p.m. UTC | #1
On Tue, 5 May 2026 at 16:07, Jonathan Wakely <jwakely@redhat.com> wrote:
>
> Clang 9 added support for [[__no_unique_address__]] and we don't support
> Intel icc any longer, so we can remove the code in <tuple> that works
> around the absence of that attribute. We can also address a FIXME in
> <bits/shared_ptr_base.h> and replace uses of EBO with the attribute.
>
> libstdc++-v3/ChangeLog:
>
>         * include/bits/shared_ptr_base.h (_Sp_ebo_helper): Simplify by
>         using [[__no_unique_address__]] instead of EBO. Use the
>         attribute unconditionally for the unstable ABI.
>         (_Sp_counted_deleter::_Impl): Adjust uses of _Sp_ebo_helper.
>         (_Sp_counted_ptr_inplace::_Impl): Likewise.
>         * include/std/tuple (_Head_base): Remove implementation for
>         compilers that don't support [[__no_unique_address__]]. Use the
>         attribute unconditionally for the unstable ABI.
> ---
>
> Tested x86_64-linux.
>
>  libstdc++-v3/include/bits/shared_ptr_base.h | 65 +++++++++----------
>  libstdc++-v3/include/std/tuple              | 69 ++++-----------------
>  2 files changed, 43 insertions(+), 91 deletions(-)
>
> diff --git a/libstdc++-v3/include/bits/shared_ptr_base.h b/libstdc++-v3/include/bits/shared_ptr_base.h
> index b92e3a4c90e4..3a966eddc06c 100644
> --- a/libstdc++-v3/include/bits/shared_ptr_base.h
> +++ b/libstdc++-v3/include/bits/shared_ptr_base.h
> @@ -513,57 +513,52 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>      inline void
>      _Sp_counted_ptr<nullptr_t, _S_atomic>::_M_dispose() noexcept { }
>
> -  // FIXME: once __has_cpp_attribute(__no_unique_address__)) is true for
> -  // all supported compilers we can greatly simplify _Sp_ebo_helper.
> +#if ! __has_cpp_attribute(__no_unique_address__)
> +#error "support for [[__no_unique_address__]] attribute is required"
> +#endif
> +
> +#if ! _GLIBCXX_INLINE_VERSION
>    // N.B. unconditionally applying the attribute could change layout for
>    // final types, which currently cannot use EBO so have a unique address.
> -
> -  template<int _Nm, typename _Tp,
> -          bool __use_ebo = !__is_final(_Tp) && __is_empty(_Tp)>
> +  template<typename _Tp, bool = !__is_final(_Tp) && __is_empty(_Tp)>
>      struct _Sp_ebo_helper;
> +#else
> +  template<typename _Tp, bool = true>
> +    struct _Sp_ebo_helper;
> +#endif
>
> -  /// Specialization using EBO.
> -  template<int _Nm, typename _Tp>
> -    struct _Sp_ebo_helper<_Nm, _Tp, true> : private _Tp
> +  /// Specialization using [[no_unique_address]].
> +  template<typename _Tp>
> +    struct _Sp_ebo_helper<_Tp, true>
>      {
> -      explicit _Sp_ebo_helper(const _Tp& __tp) : _Tp(__tp) { }
> -      explicit _Sp_ebo_helper(_Tp&& __tp) : _Tp(std::move(__tp)) { }
> -
> -      static _Tp&
> -      _S_get(_Sp_ebo_helper& __eboh) { return static_cast<_Tp&>(__eboh); }
> +      [[__no_unique_address__]] _Tp _M_obj;
>      };
>
> -  /// Specialization not using EBO.
> -  template<int _Nm, typename _Tp>
> -    struct _Sp_ebo_helper<_Nm, _Tp, false>
> +#if ! _GLIBCXX_INLINE_VERSION
> +  /// Specialization not using [[no_unique_address]].
> +  template<typename _Tp>
> +    struct _Sp_ebo_helper<_Tp, false>
>      {
> -      explicit _Sp_ebo_helper(const _Tp& __tp) : _M_tp(__tp) { }
> -      explicit _Sp_ebo_helper(_Tp&& __tp) : _M_tp(std::move(__tp)) { }
> -
> -      static _Tp&
> -      _S_get(_Sp_ebo_helper& __eboh)
> -      { return __eboh._M_tp; }
> -
> -    private:
> -      _Tp _M_tp;
> +      _Tp _M_obj;
>      };
> +#endif
>
>    // Support for custom deleter and/or allocator
>    template<typename _Ptr, typename _Deleter, typename _Alloc, _Lock_policy _Lp>
>      class _Sp_counted_deleter final : public _Sp_counted_base<_Lp>
>      {
> -      class _Impl : _Sp_ebo_helper<0, _Deleter>, _Sp_ebo_helper<1, _Alloc>
> +      class _Impl
>        {
> -       typedef _Sp_ebo_helper<0, _Deleter>     _Del_base;
> -       typedef _Sp_ebo_helper<1, _Alloc>       _Alloc_base;
> +       _Sp_ebo_helper<_Deleter> _M_d;
> +       _Sp_ebo_helper<_Alloc>   _M_a;

An off-list question from Nathan made me realize that we also need the
attribute on these members, not just inside the helper struct. I need
to check that this is actually working as intended.

>
>        public:
>         _Impl(_Ptr __p, _Deleter __d, const _Alloc& __a) noexcept
> -       : _Del_base(std::move(__d)), _Alloc_base(__a), _M_ptr(__p)
> +       : _M_d{std::move(__d)}, _M_a{__a}, _M_ptr(__p)
>         { }
>
> -       _Deleter& _M_del() noexcept { return _Del_base::_S_get(*this); }
> -       _Alloc& _M_alloc() noexcept { return _Alloc_base::_S_get(*this); }
> +       _Deleter& _M_del() noexcept { return _M_d._M_obj; }
> +       _Alloc& _M_alloc() noexcept { return _M_a._M_obj; }
>
>         _Ptr _M_ptr;
>        };
> @@ -645,14 +640,14 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>    template<typename _Tp, typename _Alloc, _Lock_policy _Lp>
>      class _Sp_counted_ptr_inplace final : public _Sp_counted_base<_Lp>
>      {
> -      class _Impl : _Sp_ebo_helper<0, _Alloc>
> +      class _Impl
>        {
> -       typedef _Sp_ebo_helper<0, _Alloc>       _A_base;
> +       _Sp_ebo_helper<_Alloc>  _M_a;
>
>        public:
> -       explicit _Impl(_Alloc __a) noexcept : _A_base(__a) { }
> +       explicit _Impl(_Alloc __a) noexcept : _M_a{std::move(__a)} { }
>
> -       _Alloc& _M_alloc() noexcept { return _A_base::_S_get(*this); }
> +       _Alloc& _M_alloc() noexcept { return _M_a._M_obj; }
>
>         __gnu_cxx::__aligned_buffer<__remove_cv_t<_Tp>> _M_storage;
>        };
> diff --git a/libstdc++-v3/include/std/tuple b/libstdc++-v3/include/std/tuple
> index f7caa79cda04..32800d8d7752 100644
> --- a/libstdc++-v3/include/std/tuple
> +++ b/libstdc++-v3/include/std/tuple
> @@ -68,7 +68,12 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>    template<typename... _Elements>
>      class tuple;
>
> +#if ! __has_cpp_attribute(__no_unique_address__)
> +#error "support for [[__no_unique_address__]] attribute is required"
> +#endif
> +
>    /// @cond undocumented
> +#if ! _GLIBCXX_INLINE_VERSION
>    template<typename _Tp>
>      struct __is_empty_non_tuple : is_empty<_Tp> { };
>
> @@ -76,17 +81,21 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>    template<typename _El0, typename... _El>
>      struct __is_empty_non_tuple<tuple<_El0, _El...>> : false_type { };
>
> -  // Use the Empty Base-class Optimization for empty, non-final types.
> +  // Use [[no_unique_address]] for empty, non-final types.
>    template<typename _Tp>
>      using __empty_not_final
>      = __conditional_t<__is_final(_Tp), false_type,
>                       __is_empty_non_tuple<_Tp>>;
> +#else
> +  // For the unstable ABI we always use [[no_unique_address]].
> +  template<typename>
> +    using __empty_not_final = true_type;
> +#endif
>
>    template<size_t _Idx, typename _Head,
>            bool = __empty_not_final<_Head>::value>
>      struct _Head_base;
>
> -#if __has_cpp_attribute(__no_unique_address__)
>    template<size_t _Idx, typename _Head>
>      struct _Head_base<_Idx, _Head, true>
>      {
> @@ -141,61 +150,8 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>
>        [[__no_unique_address__]] _Head _M_head_impl;
>      };
> -#else
> -  template<size_t _Idx, typename _Head>
> -    struct _Head_base<_Idx, _Head, true>
> -    : public _Head
> -    {
> -      constexpr _Head_base()
> -      : _Head() { }
> -
> -      constexpr _Head_base(const _Head& __h)
> -      : _Head(__h) { }
> -
> -      constexpr _Head_base(const _Head_base&) = default;
> -      constexpr _Head_base(_Head_base&&) = default;
> -
> -      template<typename _UHead>
> -        constexpr _Head_base(_UHead&& __h)
> -       : _Head(std::forward<_UHead>(__h)) { }
> -
> -      _GLIBCXX20_CONSTEXPR
> -      _Head_base(allocator_arg_t, __uses_alloc0)
> -      : _Head() { }
> -
> -      template<typename _Alloc>
> -       _GLIBCXX20_CONSTEXPR
> -       _Head_base(allocator_arg_t, __uses_alloc1<_Alloc> __a)
> -       : _Head(allocator_arg, *__a._M_a) { }
> -
> -      template<typename _Alloc>
> -       _GLIBCXX20_CONSTEXPR
> -       _Head_base(allocator_arg_t, __uses_alloc2<_Alloc> __a)
> -       : _Head(*__a._M_a) { }
> -
> -      template<typename _UHead>
> -       _GLIBCXX20_CONSTEXPR
> -       _Head_base(__uses_alloc0, _UHead&& __uhead)
> -       : _Head(std::forward<_UHead>(__uhead)) { }
> -
> -      template<typename _Alloc, typename _UHead>
> -       _GLIBCXX20_CONSTEXPR
> -       _Head_base(__uses_alloc1<_Alloc> __a, _UHead&& __uhead)
> -       : _Head(allocator_arg, *__a._M_a, std::forward<_UHead>(__uhead)) { }
> -
> -      template<typename _Alloc, typename _UHead>
> -       _GLIBCXX20_CONSTEXPR
> -       _Head_base(__uses_alloc2<_Alloc> __a, _UHead&& __uhead)
> -       : _Head(std::forward<_UHead>(__uhead), *__a._M_a) { }
> -
> -      static constexpr _Head&
> -      _M_head(_Head_base& __b) noexcept { return __b; }
> -
> -      static constexpr const _Head&
> -      _M_head(const _Head_base& __b) noexcept { return __b; }
> -    };
> -#endif
>
> +#if ! _GLIBCXX_INLINE_VERSION
>    template<size_t _Idx, typename _Head>
>      struct _Head_base<_Idx, _Head, false>
>      {
> @@ -250,6 +206,7 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>
>        _Head _M_head_impl;
>      };
> +#endif
>
>  #if __cpp_lib_tuple_like // >= C++23
>    struct __tuple_like_tag_t { explicit __tuple_like_tag_t() = default; };
> --
> 2.54.0
>
  
Tomasz Kaminski May 6, 2026, 7:54 a.m. UTC | #2
On Tue, May 5, 2026 at 5:07 PM Jonathan Wakely <jwakely@redhat.com> wrote:

> Clang 9 added support for [[__no_unique_address__]] and we don't support
> Intel icc any longer, so we can remove the code in <tuple> that works
> around the absence of that attribute. We can also address a FIXME in
> <bits/shared_ptr_base.h> and replace uses of EBO with the attribute.
>
> libstdc++-v3/ChangeLog:
>
>         * include/bits/shared_ptr_base.h (_Sp_ebo_helper): Simplify by
>         using [[__no_unique_address__]] instead of EBO. Use the
>         attribute unconditionally for the unstable ABI.
>         (_Sp_counted_deleter::_Impl): Adjust uses of _Sp_ebo_helper.
>         (_Sp_counted_ptr_inplace::_Impl): Likewise.
>         * include/std/tuple (_Head_base): Remove implementation for
>         compilers that don't support [[__no_unique_address__]]. Use the
>         attribute unconditionally for the unstable ABI.
> ---
>
> Tested x86_64-linux.
>
>  libstdc++-v3/include/bits/shared_ptr_base.h | 65 +++++++++----------
>  libstdc++-v3/include/std/tuple              | 69 ++++-----------------
>  2 files changed, 43 insertions(+), 91 deletions(-)
>
> diff --git a/libstdc++-v3/include/bits/shared_ptr_base.h
> b/libstdc++-v3/include/bits/shared_ptr_base.h
> index b92e3a4c90e4..3a966eddc06c 100644
> --- a/libstdc++-v3/include/bits/shared_ptr_base.h
> +++ b/libstdc++-v3/include/bits/shared_ptr_base.h
> @@ -513,57 +513,52 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>      inline void
>      _Sp_counted_ptr<nullptr_t, _S_atomic>::_M_dispose() noexcept { }
>
> -  // FIXME: once __has_cpp_attribute(__no_unique_address__)) is true for
> -  // all supported compilers we can greatly simplify _Sp_ebo_helper.
> +#if ! __has_cpp_attribute(__no_unique_address__)
> +#error "support for [[__no_unique_address__]] attribute is required"
> +#endif
> +
> +#if ! _GLIBCXX_INLINE_VERSION
>    // N.B. unconditionally applying the attribute could change layout for
>    // final types, which currently cannot use EBO so have a unique address.
> -
> -  template<int _Nm, typename _Tp,
> -          bool __use_ebo = !__is_final(_Tp) && __is_empty(_Tp)>
> +  template<typename _Tp, bool = !__is_final(_Tp) && __is_empty(_Tp)>
>      struct _Sp_ebo_helper;
> +#else
> +  template<typename _Tp, bool = true>
> +    struct _Sp_ebo_helper;
> +#endif
>
> -  /// Specialization using EBO.
> -  template<int _Nm, typename _Tp>
> -    struct _Sp_ebo_helper<_Nm, _Tp, true> : private _Tp
> +  /// Specialization using [[no_unique_address]].
> +  template<typename _Tp>
> +    struct _Sp_ebo_helper<_Tp, true>
>      {
> -      explicit _Sp_ebo_helper(const _Tp& __tp) : _Tp(__tp) { }
> -      explicit _Sp_ebo_helper(_Tp&& __tp) : _Tp(std::move(__tp)) { }
> -
> -      static _Tp&
> -      _S_get(_Sp_ebo_helper& __eboh) { return static_cast<_Tp&>(__eboh); }
> +      [[__no_unique_address__]] _Tp _M_obj;
>      };
>
> -  /// Specialization not using EBO.
> -  template<int _Nm, typename _Tp>
> -    struct _Sp_ebo_helper<_Nm, _Tp, false>
> +#if ! _GLIBCXX_INLINE_VERSION
> +  /// Specialization not using [[no_unique_address]].
> +  template<typename _Tp>
> +    struct _Sp_ebo_helper<_Tp, false>
>      {
> -      explicit _Sp_ebo_helper(const _Tp& __tp) : _M_tp(__tp) { }
> -      explicit _Sp_ebo_helper(_Tp&& __tp) : _M_tp(std::move(__tp)) { }
> -
> -      static _Tp&
> -      _S_get(_Sp_ebo_helper& __eboh)
> -      { return __eboh._M_tp; }
> -
> -    private:
> -      _Tp _M_tp;
> +      _Tp _M_obj;
>      };
> +#endif
>
>    // Support for custom deleter and/or allocator
>    template<typename _Ptr, typename _Deleter, typename _Alloc,
> _Lock_policy _Lp>
>      class _Sp_counted_deleter final : public _Sp_counted_base<_Lp>
>      {
> -      class _Impl : _Sp_ebo_helper<0, _Deleter>, _Sp_ebo_helper<1, _Alloc>
> +      class _Impl
>        {
> -       typedef _Sp_ebo_helper<0, _Deleter>     _Del_base;
> -       typedef _Sp_ebo_helper<1, _Alloc>       _Alloc_base;
> +       _Sp_ebo_helper<_Deleter> _M_d;
> +       _Sp_ebo_helper<_Alloc>   _M_a;
>
I have wondered if that could change the data layout if _Deleter is same as
_Alloc
(which is handled by base being indexed), but the test showed that it is
the same:
https://godbolt.org/z/o8Y9s4be4

>
>        public:
>         _Impl(_Ptr __p, _Deleter __d, const _Alloc& __a) noexcept
> -       : _Del_base(std::move(__d)), _Alloc_base(__a), _M_ptr(__p)
> +       : _M_d{std::move(__d)}, _M_a{__a}, _M_ptr(__p)
>         { }
>
> -       _Deleter& _M_del() noexcept { return _Del_base::_S_get(*this); }
> -       _Alloc& _M_alloc() noexcept { return _Alloc_base::_S_get(*this); }
> +       _Deleter& _M_del() noexcept { return _M_d._M_obj; }
> +       _Alloc& _M_alloc() noexcept { return _M_a._M_obj; }
>
>         _Ptr _M_ptr;
>        };
> @@ -645,14 +640,14 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>    template<typename _Tp, typename _Alloc, _Lock_policy _Lp>
>      class _Sp_counted_ptr_inplace final : public _Sp_counted_base<_Lp>
>      {
> -      class _Impl : _Sp_ebo_helper<0, _Alloc>
> +      class _Impl
>        {
> -       typedef _Sp_ebo_helper<0, _Alloc>       _A_base;
> +       _Sp_ebo_helper<_Alloc>  _M_a;
>
>        public:
> -       explicit _Impl(_Alloc __a) noexcept : _A_base(__a) { }
> +       explicit _Impl(_Alloc __a) noexcept : _M_a{std::move(__a)} { }
>
> -       _Alloc& _M_alloc() noexcept { return _A_base::_S_get(*this); }
> +       _Alloc& _M_alloc() noexcept { return _M_a._M_obj; }
>
>         __gnu_cxx::__aligned_buffer<__remove_cv_t<_Tp>> _M_storage;
>        };
> diff --git a/libstdc++-v3/include/std/tuple
> b/libstdc++-v3/include/std/tuple
> index f7caa79cda04..32800d8d7752 100644
> --- a/libstdc++-v3/include/std/tuple
> +++ b/libstdc++-v3/include/std/tuple
> @@ -68,7 +68,12 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>    template<typename... _Elements>
>      class tuple;
>
> +#if ! __has_cpp_attribute(__no_unique_address__)
> +#error "support for [[__no_unique_address__]] attribute is required"
> +#endif
> +
>    /// @cond undocumented
> +#if ! _GLIBCXX_INLINE_VERSION
>    template<typename _Tp>
>      struct __is_empty_non_tuple : is_empty<_Tp> { };
>
> @@ -76,17 +81,21 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>    template<typename _El0, typename... _El>
>      struct __is_empty_non_tuple<tuple<_El0, _El...>> : false_type { };
>
> -  // Use the Empty Base-class Optimization for empty, non-final types.
> +  // Use [[no_unique_address]] for empty, non-final types.
>    template<typename _Tp>
>      using __empty_not_final
>      = __conditional_t<__is_final(_Tp), false_type,
>                       __is_empty_non_tuple<_Tp>>;
> +#else
> +  // For the unstable ABI we always use [[no_unique_address]].
> +  template<typename>
> +    using __empty_not_final = true_type;
> +#endif
>
>    template<size_t _Idx, typename _Head,
>            bool = __empty_not_final<_Head>::value>
>      struct _Head_base;
>
> -#if __has_cpp_attribute(__no_unique_address__)
>    template<size_t _Idx, typename _Head>
>      struct _Head_base<_Idx, _Head, true>
>      {
> @@ -141,61 +150,8 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>
>        [[__no_unique_address__]] _Head _M_head_impl;
>      };
> -#else
> -  template<size_t _Idx, typename _Head>
> -    struct _Head_base<_Idx, _Head, true>
> -    : public _Head
> -    {
> -      constexpr _Head_base()
> -      : _Head() { }
> -
> -      constexpr _Head_base(const _Head& __h)
> -      : _Head(__h) { }
> -
> -      constexpr _Head_base(const _Head_base&) = default;
> -      constexpr _Head_base(_Head_base&&) = default;
> -
> -      template<typename _UHead>
> -        constexpr _Head_base(_UHead&& __h)
> -       : _Head(std::forward<_UHead>(__h)) { }
> -
> -      _GLIBCXX20_CONSTEXPR
> -      _Head_base(allocator_arg_t, __uses_alloc0)
> -      : _Head() { }
> -
> -      template<typename _Alloc>
> -       _GLIBCXX20_CONSTEXPR
> -       _Head_base(allocator_arg_t, __uses_alloc1<_Alloc> __a)
> -       : _Head(allocator_arg, *__a._M_a) { }
> -
> -      template<typename _Alloc>
> -       _GLIBCXX20_CONSTEXPR
> -       _Head_base(allocator_arg_t, __uses_alloc2<_Alloc> __a)
> -       : _Head(*__a._M_a) { }
> -
> -      template<typename _UHead>
> -       _GLIBCXX20_CONSTEXPR
> -       _Head_base(__uses_alloc0, _UHead&& __uhead)
> -       : _Head(std::forward<_UHead>(__uhead)) { }
> -
> -      template<typename _Alloc, typename _UHead>
> -       _GLIBCXX20_CONSTEXPR
> -       _Head_base(__uses_alloc1<_Alloc> __a, _UHead&& __uhead)
> -       : _Head(allocator_arg, *__a._M_a, std::forward<_UHead>(__uhead)) {
> }
> -
> -      template<typename _Alloc, typename _UHead>
> -       _GLIBCXX20_CONSTEXPR
> -       _Head_base(__uses_alloc2<_Alloc> __a, _UHead&& __uhead)
> -       : _Head(std::forward<_UHead>(__uhead), *__a._M_a) { }
> -
> -      static constexpr _Head&
> -      _M_head(_Head_base& __b) noexcept { return __b; }
> -
> -      static constexpr const _Head&
> -      _M_head(const _Head_base& __b) noexcept { return __b; }
> -    };
> -#endif
>
> +#if ! _GLIBCXX_INLINE_VERSION
>    template<size_t _Idx, typename _Head>
>      struct _Head_base<_Idx, _Head, false>
>      {
> @@ -250,6 +206,7 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>
>        _Head _M_head_impl;
>      };
> +#endif
>
>  #if __cpp_lib_tuple_like // >= C++23
>    struct __tuple_like_tag_t { explicit __tuple_like_tag_t() = default; };
> --
> 2.54.0
>
>
  
Jonathan Wakely May 6, 2026, 8:43 a.m. UTC | #3
On Wed, 6 May 2026 at 08:54, Tomasz Kaminski <tkaminsk@redhat.com> wrote:
>
>
>
> On Tue, May 5, 2026 at 5:07 PM Jonathan Wakely <jwakely@redhat.com> wrote:
>>
>> Clang 9 added support for [[__no_unique_address__]] and we don't support
>> Intel icc any longer, so we can remove the code in <tuple> that works
>> around the absence of that attribute. We can also address a FIXME in
>> <bits/shared_ptr_base.h> and replace uses of EBO with the attribute.
>>
>> libstdc++-v3/ChangeLog:
>>
>>         * include/bits/shared_ptr_base.h (_Sp_ebo_helper): Simplify by
>>         using [[__no_unique_address__]] instead of EBO. Use the
>>         attribute unconditionally for the unstable ABI.
>>         (_Sp_counted_deleter::_Impl): Adjust uses of _Sp_ebo_helper.
>>         (_Sp_counted_ptr_inplace::_Impl): Likewise.
>>         * include/std/tuple (_Head_base): Remove implementation for
>>         compilers that don't support [[__no_unique_address__]]. Use the
>>         attribute unconditionally for the unstable ABI.
>> ---
>>
>> Tested x86_64-linux.
>>
>>  libstdc++-v3/include/bits/shared_ptr_base.h | 65 +++++++++----------
>>  libstdc++-v3/include/std/tuple              | 69 ++++-----------------
>>  2 files changed, 43 insertions(+), 91 deletions(-)
>>
>> diff --git a/libstdc++-v3/include/bits/shared_ptr_base.h b/libstdc++-v3/include/bits/shared_ptr_base.h
>> index b92e3a4c90e4..3a966eddc06c 100644
>> --- a/libstdc++-v3/include/bits/shared_ptr_base.h
>> +++ b/libstdc++-v3/include/bits/shared_ptr_base.h
>> @@ -513,57 +513,52 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>>      inline void
>>      _Sp_counted_ptr<nullptr_t, _S_atomic>::_M_dispose() noexcept { }
>>
>> -  // FIXME: once __has_cpp_attribute(__no_unique_address__)) is true for
>> -  // all supported compilers we can greatly simplify _Sp_ebo_helper.
>> +#if ! __has_cpp_attribute(__no_unique_address__)
>> +#error "support for [[__no_unique_address__]] attribute is required"
>> +#endif
>> +
>> +#if ! _GLIBCXX_INLINE_VERSION
>>    // N.B. unconditionally applying the attribute could change layout for
>>    // final types, which currently cannot use EBO so have a unique address.
>> -
>> -  template<int _Nm, typename _Tp,
>> -          bool __use_ebo = !__is_final(_Tp) && __is_empty(_Tp)>
>> +  template<typename _Tp, bool = !__is_final(_Tp) && __is_empty(_Tp)>
>>      struct _Sp_ebo_helper;
>> +#else
>> +  template<typename _Tp, bool = true>
>> +    struct _Sp_ebo_helper;
>> +#endif
>>
>> -  /// Specialization using EBO.
>> -  template<int _Nm, typename _Tp>
>> -    struct _Sp_ebo_helper<_Nm, _Tp, true> : private _Tp
>> +  /// Specialization using [[no_unique_address]].
>> +  template<typename _Tp>
>> +    struct _Sp_ebo_helper<_Tp, true>
>>      {
>> -      explicit _Sp_ebo_helper(const _Tp& __tp) : _Tp(__tp) { }
>> -      explicit _Sp_ebo_helper(_Tp&& __tp) : _Tp(std::move(__tp)) { }
>> -
>> -      static _Tp&
>> -      _S_get(_Sp_ebo_helper& __eboh) { return static_cast<_Tp&>(__eboh); }
>> +      [[__no_unique_address__]] _Tp _M_obj;
>>      };
>>
>> -  /// Specialization not using EBO.
>> -  template<int _Nm, typename _Tp>
>> -    struct _Sp_ebo_helper<_Nm, _Tp, false>
>> +#if ! _GLIBCXX_INLINE_VERSION
>> +  /// Specialization not using [[no_unique_address]].
>> +  template<typename _Tp>
>> +    struct _Sp_ebo_helper<_Tp, false>
>>      {
>> -      explicit _Sp_ebo_helper(const _Tp& __tp) : _M_tp(__tp) { }
>> -      explicit _Sp_ebo_helper(_Tp&& __tp) : _M_tp(std::move(__tp)) { }
>> -
>> -      static _Tp&
>> -      _S_get(_Sp_ebo_helper& __eboh)
>> -      { return __eboh._M_tp; }
>> -
>> -    private:
>> -      _Tp _M_tp;
>> +      _Tp _M_obj;
>>      };
>> +#endif
>>
>>    // Support for custom deleter and/or allocator
>>    template<typename _Ptr, typename _Deleter, typename _Alloc, _Lock_policy _Lp>
>>      class _Sp_counted_deleter final : public _Sp_counted_base<_Lp>
>>      {
>> -      class _Impl : _Sp_ebo_helper<0, _Deleter>, _Sp_ebo_helper<1, _Alloc>
>> +      class _Impl
>>        {
>> -       typedef _Sp_ebo_helper<0, _Deleter>     _Del_base;
>> -       typedef _Sp_ebo_helper<1, _Alloc>       _Alloc_base;
>> +       _Sp_ebo_helper<_Deleter> _M_d;
>> +       _Sp_ebo_helper<_Alloc>   _M_a;
>
> I have wondered if that could change the data layout if _Deleter is same as _Alloc
> (which is handled by base being indexed), but the test showed that it is the same:
> https://godbolt.org/z/o8Y9s4be4

Right, but it does need the attribute on both these members, to make
_M_d._M_obj and _M_a._M_obj potentially overlapping.

https://godbolt.org/z/s1veK5jG9

Patch v2 incoming ...


>>
>>
>>        public:
>>         _Impl(_Ptr __p, _Deleter __d, const _Alloc& __a) noexcept
>> -       : _Del_base(std::move(__d)), _Alloc_base(__a), _M_ptr(__p)
>> +       : _M_d{std::move(__d)}, _M_a{__a}, _M_ptr(__p)
>>         { }
>>
>> -       _Deleter& _M_del() noexcept { return _Del_base::_S_get(*this); }
>> -       _Alloc& _M_alloc() noexcept { return _Alloc_base::_S_get(*this); }
>> +       _Deleter& _M_del() noexcept { return _M_d._M_obj; }
>> +       _Alloc& _M_alloc() noexcept { return _M_a._M_obj; }
>>
>>         _Ptr _M_ptr;
>>        };
>> @@ -645,14 +640,14 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>>    template<typename _Tp, typename _Alloc, _Lock_policy _Lp>
>>      class _Sp_counted_ptr_inplace final : public _Sp_counted_base<_Lp>
>>      {
>> -      class _Impl : _Sp_ebo_helper<0, _Alloc>
>> +      class _Impl
>>        {
>> -       typedef _Sp_ebo_helper<0, _Alloc>       _A_base;
>> +       _Sp_ebo_helper<_Alloc>  _M_a;
>>
>>        public:
>> -       explicit _Impl(_Alloc __a) noexcept : _A_base(__a) { }
>> +       explicit _Impl(_Alloc __a) noexcept : _M_a{std::move(__a)} { }
>>
>> -       _Alloc& _M_alloc() noexcept { return _A_base::_S_get(*this); }
>> +       _Alloc& _M_alloc() noexcept { return _M_a._M_obj; }
>>
>>         __gnu_cxx::__aligned_buffer<__remove_cv_t<_Tp>> _M_storage;
>>        };
>> diff --git a/libstdc++-v3/include/std/tuple b/libstdc++-v3/include/std/tuple
>> index f7caa79cda04..32800d8d7752 100644
>> --- a/libstdc++-v3/include/std/tuple
>> +++ b/libstdc++-v3/include/std/tuple
>> @@ -68,7 +68,12 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>>    template<typename... _Elements>
>>      class tuple;
>>
>> +#if ! __has_cpp_attribute(__no_unique_address__)
>> +#error "support for [[__no_unique_address__]] attribute is required"
>> +#endif
>> +
>>    /// @cond undocumented
>> +#if ! _GLIBCXX_INLINE_VERSION
>>    template<typename _Tp>
>>      struct __is_empty_non_tuple : is_empty<_Tp> { };
>>
>> @@ -76,17 +81,21 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>>    template<typename _El0, typename... _El>
>>      struct __is_empty_non_tuple<tuple<_El0, _El...>> : false_type { };
>>
>> -  // Use the Empty Base-class Optimization for empty, non-final types.
>> +  // Use [[no_unique_address]] for empty, non-final types.
>>    template<typename _Tp>
>>      using __empty_not_final
>>      = __conditional_t<__is_final(_Tp), false_type,
>>                       __is_empty_non_tuple<_Tp>>;
>> +#else
>> +  // For the unstable ABI we always use [[no_unique_address]].
>> +  template<typename>
>> +    using __empty_not_final = true_type;
>> +#endif
>>
>>    template<size_t _Idx, typename _Head,
>>            bool = __empty_not_final<_Head>::value>
>>      struct _Head_base;
>>
>> -#if __has_cpp_attribute(__no_unique_address__)
>>    template<size_t _Idx, typename _Head>
>>      struct _Head_base<_Idx, _Head, true>
>>      {
>> @@ -141,61 +150,8 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>>
>>        [[__no_unique_address__]] _Head _M_head_impl;
>>      };
>> -#else
>> -  template<size_t _Idx, typename _Head>
>> -    struct _Head_base<_Idx, _Head, true>
>> -    : public _Head
>> -    {
>> -      constexpr _Head_base()
>> -      : _Head() { }
>> -
>> -      constexpr _Head_base(const _Head& __h)
>> -      : _Head(__h) { }
>> -
>> -      constexpr _Head_base(const _Head_base&) = default;
>> -      constexpr _Head_base(_Head_base&&) = default;
>> -
>> -      template<typename _UHead>
>> -        constexpr _Head_base(_UHead&& __h)
>> -       : _Head(std::forward<_UHead>(__h)) { }
>> -
>> -      _GLIBCXX20_CONSTEXPR
>> -      _Head_base(allocator_arg_t, __uses_alloc0)
>> -      : _Head() { }
>> -
>> -      template<typename _Alloc>
>> -       _GLIBCXX20_CONSTEXPR
>> -       _Head_base(allocator_arg_t, __uses_alloc1<_Alloc> __a)
>> -       : _Head(allocator_arg, *__a._M_a) { }
>> -
>> -      template<typename _Alloc>
>> -       _GLIBCXX20_CONSTEXPR
>> -       _Head_base(allocator_arg_t, __uses_alloc2<_Alloc> __a)
>> -       : _Head(*__a._M_a) { }
>> -
>> -      template<typename _UHead>
>> -       _GLIBCXX20_CONSTEXPR
>> -       _Head_base(__uses_alloc0, _UHead&& __uhead)
>> -       : _Head(std::forward<_UHead>(__uhead)) { }
>> -
>> -      template<typename _Alloc, typename _UHead>
>> -       _GLIBCXX20_CONSTEXPR
>> -       _Head_base(__uses_alloc1<_Alloc> __a, _UHead&& __uhead)
>> -       : _Head(allocator_arg, *__a._M_a, std::forward<_UHead>(__uhead)) { }
>> -
>> -      template<typename _Alloc, typename _UHead>
>> -       _GLIBCXX20_CONSTEXPR
>> -       _Head_base(__uses_alloc2<_Alloc> __a, _UHead&& __uhead)
>> -       : _Head(std::forward<_UHead>(__uhead), *__a._M_a) { }
>> -
>> -      static constexpr _Head&
>> -      _M_head(_Head_base& __b) noexcept { return __b; }
>> -
>> -      static constexpr const _Head&
>> -      _M_head(const _Head_base& __b) noexcept { return __b; }
>> -    };
>> -#endif
>>
>> +#if ! _GLIBCXX_INLINE_VERSION
>>    template<size_t _Idx, typename _Head>
>>      struct _Head_base<_Idx, _Head, false>
>>      {
>> @@ -250,6 +206,7 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
>>
>>        _Head _M_head_impl;
>>      };
>> +#endif
>>
>>  #if __cpp_lib_tuple_like // >= C++23
>>    struct __tuple_like_tag_t { explicit __tuple_like_tag_t() = default; };
>> --
>> 2.54.0
>>
  

Patch

diff --git a/libstdc++-v3/include/bits/shared_ptr_base.h b/libstdc++-v3/include/bits/shared_ptr_base.h
index b92e3a4c90e4..3a966eddc06c 100644
--- a/libstdc++-v3/include/bits/shared_ptr_base.h
+++ b/libstdc++-v3/include/bits/shared_ptr_base.h
@@ -513,57 +513,52 @@  _GLIBCXX_BEGIN_NAMESPACE_VERSION
     inline void
     _Sp_counted_ptr<nullptr_t, _S_atomic>::_M_dispose() noexcept { }
 
-  // FIXME: once __has_cpp_attribute(__no_unique_address__)) is true for
-  // all supported compilers we can greatly simplify _Sp_ebo_helper.
+#if ! __has_cpp_attribute(__no_unique_address__)
+#error "support for [[__no_unique_address__]] attribute is required"
+#endif
+
+#if ! _GLIBCXX_INLINE_VERSION
   // N.B. unconditionally applying the attribute could change layout for
   // final types, which currently cannot use EBO so have a unique address.
-
-  template<int _Nm, typename _Tp,
-	   bool __use_ebo = !__is_final(_Tp) && __is_empty(_Tp)>
+  template<typename _Tp, bool = !__is_final(_Tp) && __is_empty(_Tp)>
     struct _Sp_ebo_helper;
+#else
+  template<typename _Tp, bool = true>
+    struct _Sp_ebo_helper;
+#endif
 
-  /// Specialization using EBO.
-  template<int _Nm, typename _Tp>
-    struct _Sp_ebo_helper<_Nm, _Tp, true> : private _Tp
+  /// Specialization using [[no_unique_address]].
+  template<typename _Tp>
+    struct _Sp_ebo_helper<_Tp, true>
     {
-      explicit _Sp_ebo_helper(const _Tp& __tp) : _Tp(__tp) { }
-      explicit _Sp_ebo_helper(_Tp&& __tp) : _Tp(std::move(__tp)) { }
-
-      static _Tp&
-      _S_get(_Sp_ebo_helper& __eboh) { return static_cast<_Tp&>(__eboh); }
+      [[__no_unique_address__]] _Tp _M_obj;
     };
 
-  /// Specialization not using EBO.
-  template<int _Nm, typename _Tp>
-    struct _Sp_ebo_helper<_Nm, _Tp, false>
+#if ! _GLIBCXX_INLINE_VERSION
+  /// Specialization not using [[no_unique_address]].
+  template<typename _Tp>
+    struct _Sp_ebo_helper<_Tp, false>
     {
-      explicit _Sp_ebo_helper(const _Tp& __tp) : _M_tp(__tp) { }
-      explicit _Sp_ebo_helper(_Tp&& __tp) : _M_tp(std::move(__tp)) { }
-
-      static _Tp&
-      _S_get(_Sp_ebo_helper& __eboh)
-      { return __eboh._M_tp; }
-
-    private:
-      _Tp _M_tp;
+      _Tp _M_obj;
     };
+#endif
 
   // Support for custom deleter and/or allocator
   template<typename _Ptr, typename _Deleter, typename _Alloc, _Lock_policy _Lp>
     class _Sp_counted_deleter final : public _Sp_counted_base<_Lp>
     {
-      class _Impl : _Sp_ebo_helper<0, _Deleter>, _Sp_ebo_helper<1, _Alloc>
+      class _Impl
       {
-	typedef _Sp_ebo_helper<0, _Deleter>	_Del_base;
-	typedef _Sp_ebo_helper<1, _Alloc>	_Alloc_base;
+	_Sp_ebo_helper<_Deleter> _M_d;
+	_Sp_ebo_helper<_Alloc>   _M_a;
 
       public:
 	_Impl(_Ptr __p, _Deleter __d, const _Alloc& __a) noexcept
-	: _Del_base(std::move(__d)), _Alloc_base(__a), _M_ptr(__p)
+	: _M_d{std::move(__d)}, _M_a{__a}, _M_ptr(__p)
 	{ }
 
-	_Deleter& _M_del() noexcept { return _Del_base::_S_get(*this); }
-	_Alloc& _M_alloc() noexcept { return _Alloc_base::_S_get(*this); }
+	_Deleter& _M_del() noexcept { return _M_d._M_obj; }
+	_Alloc& _M_alloc() noexcept { return _M_a._M_obj; }
 
 	_Ptr _M_ptr;
       };
@@ -645,14 +640,14 @@  _GLIBCXX_BEGIN_NAMESPACE_VERSION
   template<typename _Tp, typename _Alloc, _Lock_policy _Lp>
     class _Sp_counted_ptr_inplace final : public _Sp_counted_base<_Lp>
     {
-      class _Impl : _Sp_ebo_helper<0, _Alloc>
+      class _Impl
       {
-	typedef _Sp_ebo_helper<0, _Alloc>	_A_base;
+	_Sp_ebo_helper<_Alloc>	_M_a;
 
       public:
-	explicit _Impl(_Alloc __a) noexcept : _A_base(__a) { }
+	explicit _Impl(_Alloc __a) noexcept : _M_a{std::move(__a)} { }
 
-	_Alloc& _M_alloc() noexcept { return _A_base::_S_get(*this); }
+	_Alloc& _M_alloc() noexcept { return _M_a._M_obj; }
 
 	__gnu_cxx::__aligned_buffer<__remove_cv_t<_Tp>> _M_storage;
       };
diff --git a/libstdc++-v3/include/std/tuple b/libstdc++-v3/include/std/tuple
index f7caa79cda04..32800d8d7752 100644
--- a/libstdc++-v3/include/std/tuple
+++ b/libstdc++-v3/include/std/tuple
@@ -68,7 +68,12 @@  _GLIBCXX_BEGIN_NAMESPACE_VERSION
   template<typename... _Elements>
     class tuple;
 
+#if ! __has_cpp_attribute(__no_unique_address__)
+#error "support for [[__no_unique_address__]] attribute is required"
+#endif
+
   /// @cond undocumented
+#if ! _GLIBCXX_INLINE_VERSION
   template<typename _Tp>
     struct __is_empty_non_tuple : is_empty<_Tp> { };
 
@@ -76,17 +81,21 @@  _GLIBCXX_BEGIN_NAMESPACE_VERSION
   template<typename _El0, typename... _El>
     struct __is_empty_non_tuple<tuple<_El0, _El...>> : false_type { };
 
-  // Use the Empty Base-class Optimization for empty, non-final types.
+  // Use [[no_unique_address]] for empty, non-final types.
   template<typename _Tp>
     using __empty_not_final
     = __conditional_t<__is_final(_Tp), false_type,
 		      __is_empty_non_tuple<_Tp>>;
+#else
+  // For the unstable ABI we always use [[no_unique_address]].
+  template<typename>
+    using __empty_not_final = true_type;
+#endif
 
   template<size_t _Idx, typename _Head,
 	   bool = __empty_not_final<_Head>::value>
     struct _Head_base;
 
-#if __has_cpp_attribute(__no_unique_address__)
   template<size_t _Idx, typename _Head>
     struct _Head_base<_Idx, _Head, true>
     {
@@ -141,61 +150,8 @@  _GLIBCXX_BEGIN_NAMESPACE_VERSION
 
       [[__no_unique_address__]] _Head _M_head_impl;
     };
-#else
-  template<size_t _Idx, typename _Head>
-    struct _Head_base<_Idx, _Head, true>
-    : public _Head
-    {
-      constexpr _Head_base()
-      : _Head() { }
-
-      constexpr _Head_base(const _Head& __h)
-      : _Head(__h) { }
-
-      constexpr _Head_base(const _Head_base&) = default;
-      constexpr _Head_base(_Head_base&&) = default;
-
-      template<typename _UHead>
-        constexpr _Head_base(_UHead&& __h)
-	: _Head(std::forward<_UHead>(__h)) { }
-
-      _GLIBCXX20_CONSTEXPR
-      _Head_base(allocator_arg_t, __uses_alloc0)
-      : _Head() { }
-
-      template<typename _Alloc>
-	_GLIBCXX20_CONSTEXPR
-	_Head_base(allocator_arg_t, __uses_alloc1<_Alloc> __a)
-	: _Head(allocator_arg, *__a._M_a) { }
-
-      template<typename _Alloc>
-	_GLIBCXX20_CONSTEXPR
-	_Head_base(allocator_arg_t, __uses_alloc2<_Alloc> __a)
-	: _Head(*__a._M_a) { }
-
-      template<typename _UHead>
-	_GLIBCXX20_CONSTEXPR
-	_Head_base(__uses_alloc0, _UHead&& __uhead)
-	: _Head(std::forward<_UHead>(__uhead)) { }
-
-      template<typename _Alloc, typename _UHead>
-	_GLIBCXX20_CONSTEXPR
-	_Head_base(__uses_alloc1<_Alloc> __a, _UHead&& __uhead)
-	: _Head(allocator_arg, *__a._M_a, std::forward<_UHead>(__uhead)) { }
-
-      template<typename _Alloc, typename _UHead>
-	_GLIBCXX20_CONSTEXPR
-	_Head_base(__uses_alloc2<_Alloc> __a, _UHead&& __uhead)
-	: _Head(std::forward<_UHead>(__uhead), *__a._M_a) { }
-
-      static constexpr _Head&
-      _M_head(_Head_base& __b) noexcept { return __b; }
-
-      static constexpr const _Head&
-      _M_head(const _Head_base& __b) noexcept { return __b; }
-    };
-#endif
 
+#if ! _GLIBCXX_INLINE_VERSION
   template<size_t _Idx, typename _Head>
     struct _Head_base<_Idx, _Head, false>
     {
@@ -250,6 +206,7 @@  _GLIBCXX_BEGIN_NAMESPACE_VERSION
 
       _Head _M_head_impl;
     };
+#endif
 
 #if __cpp_lib_tuple_like // >= C++23
   struct __tuple_like_tag_t { explicit __tuple_like_tag_t() = default; };