On Mon, 18 Nov 2024 at 18:32, François Dumont <frs.dum...@gmail.com> wrote:
>
>
> On 18/11/2024 19:24, François Dumont wrote:
> >
> > On 16/11/2024 02:18, Jonathan Wakely wrote:
> >> On Sat, 16 Nov 2024 at 01:09, Jonathan Wakely <jwak...@redhat.com>
> >> wrote:
> >>> While working on fancy pointer support for the linked lists I noticed
> >>> they didn't have any debug assertions. This adds the obvious non-empty
> >>> assertions to front(), back(), pop_front() and pop_back().
> >>>
> >>> For the pop members, adding an assertion to the underlying function
> >>> that
> >>> erases a member means it also check erase(end()), which is always
> >>> invalid, and erase(begin()) on an empty list. For those erase
> >>> members we
> >>> can also add a check so that we return without doing anything if the
> >>> assertion is disabled, but would have failed had it been enabled.
> >>>
> >>> libstdc++-v3/ChangeLog:
> >>>
> >>>          * include/bits/forward_list.h (forward_list::front): Add
> >>>          non-empty assertions.
> >>>          * include/bits/forward_list.tcc
> >>> (_Fwd_list_base::_M_erase_after):
> >>>          Likewise. Return immediately if argument is invalid.
> >>>          * include/bits/stl_list.h (list::front, list::back): Add
> >>>          non-empty assertions.
> >>>          (list::_M_erase): Likewise. Return immediately if argument is
> >>>          invalid.
> >>> ---
> >>>
> >>> Tested x86_64-linux.
> >>>
> >>> As pull request: https://forge.sourceware.org/gcc/gcc-TEST/pulls/26
> >>>
> >>>   libstdc++-v3/include/bits/forward_list.h   |  3 +++
> >>>   libstdc++-v3/include/bits/forward_list.tcc |  6 ++++++
> >>>   libstdc++-v3/include/bits/stl_list.h       | 19 +++++++++++++++++--
> >>>   3 files changed, 26 insertions(+), 2 deletions(-)
> >>>
> >>> diff --git a/libstdc++-v3/include/bits/forward_list.h
> >>> b/libstdc++-v3/include/bits/forward_list.h
> >>> index c9238cef96f..3fac657518c 100644
> >>> --- a/libstdc++-v3/include/bits/forward_list.h
> >>> +++ b/libstdc++-v3/include/bits/forward_list.h
> >>> @@ -42,6 +42,7 @@
> >>>   #include <bits/allocator.h>
> >>>   #include <ext/alloc_traits.h>
> >>>   #include <ext/aligned_buffer.h>
> >>> +#include <debug/assertions.h>
> >>>   #if __glibcxx_ranges_to_container // C++ >= 23
> >>>   # include <bits/ranges_base.h> // ranges::begin, ranges::distance
> >>> etc.
> >>>   # include <bits/ranges_util.h> // ranges::subrange
> >>> @@ -884,6 +885,7 @@ _GLIBCXX_BEGIN_NAMESPACE_CONTAINER
> >>>         reference
> >>>         front()
> >>>         {
> >>> +       __glibcxx_requires_nonempty();
> >>>          _Node* __front =
> >>> static_cast<_Node*>(this->_M_impl._M_head._M_next);
> >>>          return *__front->_M_valptr();
> >>>         }
> >>> @@ -896,6 +898,7 @@ _GLIBCXX_BEGIN_NAMESPACE_CONTAINER
> >>>         const_reference
> >>>         front() const
> >>>         {
> >>> +       __glibcxx_requires_nonempty();
> >>>          _Node* __front =
> >>> static_cast<_Node*>(this->_M_impl._M_head._M_next);
> >>>          return *__front->_M_valptr();
> >>>         }
> >>> diff --git a/libstdc++-v3/include/bits/forward_list.tcc
> >>> b/libstdc++-v3/include/bits/forward_list.tcc
> >>> index 9750c7c0502..50acdb9f26b 100644
> >>> --- a/libstdc++-v3/include/bits/forward_list.tcc
> >>> +++ b/libstdc++-v3/include/bits/forward_list.tcc
> >>> @@ -63,6 +63,12 @@ _GLIBCXX_BEGIN_NAMESPACE_CONTAINER
> >>>       _Fwd_list_base<_Tp, _Alloc>::
> >>>       _M_erase_after(_Fwd_list_node_base* __pos)
> >>>       {
> >>> +      if (__pos == nullptr || __pos->_M_next == nullptr)
> >>> [[__unlikely__]]
> >>> +       {
> >>> +         __glibcxx_assert(__pos != nullptr && __pos->_M_next !=
> >>> nullptr);
> >>> +         return nullptr;
> >>> +       }
> >>> +
> >>>         _Node* __curr = static_cast<_Node*>(__pos->_M_next);
> >>>         __pos->_M_next = __curr->_M_next;
> >>>         _Node_alloc_traits::destroy(_M_get_Node_allocator(),
> >>> diff --git a/libstdc++-v3/include/bits/stl_list.h
> >>> b/libstdc++-v3/include/bits/stl_list.h
> >>> index 7deb04b4bfe..d70ba90b8fa 100644
> >>> --- a/libstdc++-v3/include/bits/stl_list.h
> >>> +++ b/libstdc++-v3/include/bits/stl_list.h
> >>> @@ -59,6 +59,7 @@
> >>>
> >>>   #include <bits/concept_check.h>
> >>>   #include <ext/alloc_traits.h>
> >>> +#include <debug/assertions.h>
> >>>   #if __cplusplus >= 201103L
> >>>   #include <initializer_list>
> >>>   #include <bits/allocated_ptr.h>
> >>> @@ -1249,7 +1250,10 @@ _GLIBCXX_BEGIN_NAMESPACE_CXX11
> >>>         _GLIBCXX_NODISCARD
> >>>         reference
> >>>         front() _GLIBCXX_NOEXCEPT
> >>> -      { return *begin(); }
> >>> +      {
> >>> +       __glibcxx_requires_nonempty();
> >>> +       return *begin();
> >>> +      }
> >>>
> >>>         /**
> >>>          *  Returns a read-only (constant) reference to the data at
> >>> the first
> >>> @@ -1258,7 +1262,10 @@ _GLIBCXX_BEGIN_NAMESPACE_CXX11
> >>>         _GLIBCXX_NODISCARD
> >>>         const_reference
> >>>         front() const _GLIBCXX_NOEXCEPT
> >>> -      { return *begin(); }
> >>> +      {
> >>> +       __glibcxx_requires_nonempty();
> >>> +       return *begin();
> >>> +      }
> >>>
> >>>         /**
> >>>          *  Returns a read/write reference to the data at the last
> >>> element
> >>> @@ -1268,6 +1275,7 @@ _GLIBCXX_BEGIN_NAMESPACE_CXX11
> >>>         reference
> >>>         back() _GLIBCXX_NOEXCEPT
> >>>         {
> >>> +       __glibcxx_requires_nonempty();
> >>>          iterator __tmp = end();
> >>>          --__tmp;
> >>>          return *__tmp;
> >>> @@ -1281,6 +1289,7 @@ _GLIBCXX_BEGIN_NAMESPACE_CXX11
> >>>         const_reference
> >>>         back() const _GLIBCXX_NOEXCEPT
> >>>         {
> >>> +       __glibcxx_requires_nonempty();
> >>>          const_iterator __tmp = end();
> >>>          --__tmp;
> >>>          return *__tmp;
> >>> @@ -2132,6 +2141,12 @@ _GLIBCXX_BEGIN_NAMESPACE_CXX11
> >>>         void
> >>>         _M_erase(iterator __position) _GLIBCXX_NOEXCEPT
> >>>         {
> >>> +       if (__builtin_expect(empty(), 0))
> >>> +         {
> >>> +           __glibcxx_requires_nonempty();
> >>> +           return;
> >>> +         }
> >> Hmm, I'm having second thoughts about the "return without doing
> >> anything part now.
> >> For this simple test:
> >>
> >> #include <list>
> >>
> >> int main()
> >> {
> >>   std::list<int> l;
> >>   l.erase(l.begin());
> >> }
> >>
> >> Currently it crashes (bad), but with -O1 there's a nice warning:
> >>
> >> /usr/include/c++/14/bits/new_allocator.h:172:33: warning: ‘void
> >> operator delete(void*, std::size_t)’ called on unallocated object ‘l’
> >> [-Wfree-nonheap-object]
> >>
> >> And Asan can diagnose it too.
> >>
> >> Adding an assertion is definitely an improvement, as it avoids the
> >> crash . But returning when the assertion is disabled, so that the
> >> function is a no-op, means that the warning about freeing a null
> >> pointer goes away, because the compiler can see it's never reached.
> >> And now Asan can't diagnose it.
> >>
> >> I think on balance, making it a no-op and avoiding arbitrary UB is
> >> better. The warning is only possible in trivial cases where the
> >> compiler can see the pointer is definitely null. In more realistic
> >> code, there will be no warning, and UB, so turning it into a silent
> >> no-op does seem safer. If you want to detect the bug, enable
> >> assertions.
> >>
> >> What do others think? Better to add the assertion but leave the UB
> >> present when assertions are disabled, or add the assertion and
> >> silently remove the UB when assertions are enabled?
> >
> > I think it depends on the impact on performances, is there any ?
> >
> > You're not using the [__unlikely__] attribute to avoid the UB I guess
> > but maybe at a cost.

I used __builtin_expect instead of [[__unlikely__]] because that's
valid in C++98 (GCC and recent versions of Clang now allow [[attr]] in
C++98 too, so at some point we could switch to using those attributes
everywhere).

There will be a non-zero cost for correct code, because of an extra
check. But std::list is not a high-performance container anyway, and
avoiding UB might be worth the overhead.


> >
> On a second thought it rather depends on the call context.
>
> This _M_erase is called from 3 locations, pop_front, pop_back and erase.
>
> For pop_front/pop_back I think the UB should be preserved.

Do you mean no assertions?

> For erase,
> silently ignoring a erase(begin()) seems more user friendly.
>
> So maybe the assertions should be moved to the public methods and
> adapted to this context.

Yes, I came to the same conclusion.

I have a simpler version of the patch which only adds nonempty
assertions to front() and back(). I'll work on adding erase and splice
assertions separately (and maybe not for GCC 15).

Reply via email to