On Mon, 24 Mar 2025 at 14:18, Patrick Palka <ppa...@redhat.com> wrote:
>
> On Mon, 24 Mar 2025, Jonathan Wakely wrote:
>
> > Unlike insert_range and assign_range, the append_range function does not
> > have a precondition that the range doesn't overlap *this. That means we
> > need to avoid relocating the existing elements until after copying from
> > the range. This means I need to revert r15-8488-g3e1d760bf49d0e which
> > made the from_range_t constructor use append_range, because the
> > constructor can avoid the additional complexity needed by append_range.
> > When relocating the existing elements in append_range we can use
> > std::__relocate_a to do it more efficiently, if that's valid.
> >
> > std::vector<bool>::append_range needs similar treatment, although it's a
> > bit simpler as we know that the elements are trivially copyable and so
> > we don't need to worry about them throwing. assign_range doesn't allow
> > overlapping ranges, so can be rewritten to be more efficient than
> > calling append_range for the forward or sized range case.
> >
> > libstdc++-v3/ChangeLog:
> >
> >       * include/bits/stl_bvector.h (vector::assign_range): More
> >       efficient implementation for forward/sized ranges.
> >       (vector::append_range): Handle potentially overlapping range.
> >       * include/bits/stl_vector.h (vector(from_range_t, R&&, Alloc)):
> >       Do not use append_range for non-sized input range case.
> >       (vector::append_range)
> >       (vector::append_range): Handle potentially overlapping range.
> >       * include/bits/vector.tcc (vector::insert_range): Forward range
> >       instead of moving it.
> >       * 
> > testsuite/23_containers/vector/bool/modifiers/insert/append_range.cc:
> >       Test overlapping ranges.
> >       * testsuite/23_containers/vector/modifiers/append_range.cc:
> >       Likewise.
> > ---
> >
> > Changed vector<bool, A>::assign_range to just use a loop for the
> > non-sized input range case, instead of calling append_range.
> >
> > Changed private copy ctor to deleted move ctor in RAII type.
> >
> > Testing x86_64-linux now.
> >
> >  libstdc++-v3/include/bits/stl_bvector.h       |  69 ++++++--
> >  libstdc++-v3/include/bits/stl_vector.h        | 101 ++++++++++-
> >  libstdc++-v3/include/bits/vector.tcc          |   2 +-
> >  .../bool/modifiers/insert/append_range.cc     |  50 ++++++
> >  .../vector/modifiers/append_range.cc          | 164 ++++++++++++++++++
> >  5 files changed, 366 insertions(+), 20 deletions(-)
> >
> > diff --git a/libstdc++-v3/include/bits/stl_bvector.h 
> > b/libstdc++-v3/include/bits/stl_bvector.h
> > index 3ee15eaa938..5addab048c9 100644
> > --- a/libstdc++-v3/include/bits/stl_bvector.h
> > +++ b/libstdc++-v3/include/bits/stl_bvector.h
> > @@ -1035,8 +1035,25 @@ _GLIBCXX_BEGIN_NAMESPACE_CONTAINER
> >       assign_range(_Rg&& __rg)
> >       {
> >         static_assert(assignable_from<bool&, 
> > ranges::range_reference_t<_Rg>>);
> > -       clear();
> > -       append_range(std::forward<_Rg>(__rg));
> > +       if constexpr (ranges::forward_range<_Rg> || 
> > ranges::sized_range<_Rg>)
> > +         {
> > +           if (auto __n = size_type(ranges::distance(__rg)))
> > +             {
> > +               reserve(__n);
> > +               this->_M_impl._M_finish
> > +                   = ranges::copy(std::forward<_Rg>(__rg), begin()).out;
> > +             }
> > +           else
> > +             clear();
> > +         }
> > +       else
> > +         {
> > +           clear();
> > +           auto __first = ranges::begin(__rg);
> > +           const auto __last = ranges::end(__rg);
> > +           for (; __first != __last; ++__first)
> > +             emplace_back(*__first);
> > +         }
> >       }
> >  #endif
> >
> > @@ -1385,24 +1402,52 @@ _GLIBCXX_BEGIN_NAMESPACE_CONTAINER
> >       constexpr void
> >       append_range(_Rg&& __rg)
> >       {
> > +       // N.B. append_range(r) allows r to overlap with *this,
> > +       // e.g. it could be *this | views::filter(pred) | views::to_input
> > +       // and so we must not invalidate existing elements too soon.
> >         if constexpr (ranges::forward_range<_Rg> || 
> > ranges::sized_range<_Rg>)
> >           {
> > -           reserve(size() + size_type(ranges::distance(__rg)));
> > -           this->_M_impl._M_finish = ranges::copy(__rg, end()).out;
> > +           const auto __n = size_type(ranges::distance(__rg));
> > +           const auto __sz = size();
> > +
> > +           // If there are no existing elements it's safe to allocate now.
> > +           if (__sz == 0)
> > +             reserve(__n);
> > +
> > +           const auto __capacity = capacity();
> > +           if ((__capacity - __sz) >= __n)
> > +             {
> > +               this->_M_impl._M_finish
> > +                   = ranges::copy(std::forward<_Rg>(__rg), end()).out;
> > +               return;
> > +             }
> > +
> > +           vector __tmp(get_allocator());
> > +           __tmp.reserve(_M_check_len(__n, "vector::append_range"));
> > +           __tmp._M_impl._M_finish
> > +                = _M_copy_aligned(cbegin(), cend(), __tmp.begin());
> > +           __tmp._M_impl._M_finish
> > +                = ranges::copy(std::forward<_Rg>(__rg), __tmp.end()).out;
> > +           swap(__tmp);
> >           }
> >         else
> >           {
> >             auto __first = ranges::begin(__rg);
> >             const auto __last = ranges::end(__rg);
> > -           size_type __n = size();
> > -           const size_type __cap = capacity();
> > -           for (; __first != __last && __n < __cap; ++__first, (void)++__n)
> > +
> > +           // Fill up to the end of current capacity.
> > +           for (auto __free = capacity() - size();
> > +                __first != __last && __free > 0;
> > +                ++__first, (void) --__free)
> >               emplace_back(*__first);
> > -           if (__first != __last)
> > -             {
> > -               ranges::subrange __rest(std::move(__first), __last);
> > -               append_range(vector(from_range, __rest, get_allocator()));
> > -             }
> > +
> > +           if (__first == __last)
> > +             return;
> > +
> > +           // Copy the rest of the range to a new vector.
> > +           ranges::subrange __rest(std::move(__first), __last);
> > +           vector __tmp(from_range, __rest, get_allocator());
> > +           insert(end(), __tmp.begin(), __tmp.end());
> >           }
> >       }
> >  #endif // ranges_to_container
> > diff --git a/libstdc++-v3/include/bits/stl_vector.h 
> > b/libstdc++-v3/include/bits/stl_vector.h
> > index 09fd53696d1..7f0b62e9b93 100644
> > --- a/libstdc++-v3/include/bits/stl_vector.h
> > +++ b/libstdc++-v3/include/bits/stl_vector.h
> > @@ -65,7 +65,7 @@
> >  #if __cplusplus >= 202002L
> >  # include <compare>
> >  #endif
> > -#if __cplusplus > 202002L
> > +#if __glibcxx_ranges_to_container // C++ >= 23
> >  # include <bits/ranges_algobase.h>      // ranges::copy
> >  # include <bits/ranges_util.h>          // ranges::subrange
> >  #endif
> > @@ -771,7 +771,12 @@ _GLIBCXX_BEGIN_NAMESPACE_CONTAINER
> >             _Base::_M_append_range(__rg);
> >           }
> >         else
> > -         append_range(std::move(__rg));
> > +         {
> > +           auto __first = ranges::begin(__rg);
> > +           const auto __last = ranges::end(__rg);
> > +           for (; __first != __last; ++__first)
> > +             emplace_back(*__first);
> > +         }
> >       }
> >  #endif
> >
> > @@ -1648,20 +1653,102 @@ _GLIBCXX_BEGIN_NAMESPACE_CONTAINER
> >       constexpr void
> >       append_range(_Rg&& __rg)
> >       {
> > +       // N.B. append_range(r) allows r to overlap with *this,
> > +       // e.g. it could be *this | views::filter(pred) | views::to_input
> > +       // and so we must not invalidate existing elements too soon.
> >         if constexpr (ranges::forward_range<_Rg> || 
> > ranges::sized_range<_Rg>)
> >           {
> >             const auto __n = size_type(ranges::distance(__rg));
> > -           reserve(size() + __n);
> > -           _GLIBCXX_ASAN_ANNOTATE_GROW(__n);
> > -           _Base::_M_append_range(__rg);
> > -           _GLIBCXX_ASAN_ANNOTATE_GREW(__n);
> > +           const auto __sz = size();
> > +
> > +           // If there are no existing elements it's safe to allocate now.
> > +           if (__sz == 0)
> > +             reserve(__n);
> > +
> > +           const auto __capacity = capacity();
> > +           if ((__capacity - __sz) >= __n)
> > +             {
> > +               _GLIBCXX_ASAN_ANNOTATE_GROW(__n);
> > +               _Base::_M_append_range(__rg);
> > +               _GLIBCXX_ASAN_ANNOTATE_GREW(__n);
> > +               return;
> > +             }
> > +
> > +           const size_type __len = _M_check_len(__n, 
> > "vector::append_range");
> > +
> > +           pointer __old_start = this->_M_impl._M_start;
> > +           pointer __old_finish = this->_M_impl._M_finish;
> > +
> > +           allocator_type& __a = _M_get_Tp_allocator();
> > +           const pointer __start = this->_M_allocate(__len);
> > +           const pointer __mid = __start + __sz;
> > +           const pointer __back = __mid + __n;
> > +           _Guard_alloc __guard(__start, __len, *this);
> > +           std::__uninitialized_copy_a(ranges::begin(__rg),
> > +                                       ranges::end(__rg),
> > +                                       __mid, __a);
> > +
> > +           if constexpr (_S_use_relocate())
> > +             _S_relocate(__old_start, __old_finish, __start, __a);
> > +           else
> > +             {
> > +               // RAII type to destroy initialized elements.
> > +               struct _Guard_elts
> > +               {
> > +                 pointer _M_first, _M_last;  // Elements to destroy
> > +                 _Tp_alloc_type& _M_alloc;
> > +
> > +                 constexpr
> > +                 _Guard_elts(pointer __f, pointer __l, _Tp_alloc_type& __a)
> > +                 : _M_first(__f), _M_last(__l), _M_alloc(__a)
> > +                 { }
> > +
> > +                 constexpr
> > +                 ~_Guard_elts()
> > +                 { std::_Destroy(_M_first, _M_last, _M_alloc); }
> > +
> > +                 _Guard_elts(_Guard_elts&&) = delete;
> > +               };
> > +               _Guard_elts __guard_elts{__mid, __back, __a};
> > +
> > +               std::__uninitialized_move_a(__old_start, __old_finish,
> > +                                           __start, __a);
> > +
> > +               // Let old elements get destroyed by __guard_elts:
> > +               __guard_elts._M_first = __old_start;
> > +               __guard_elts._M_last = __old_finish;
> > +             }
> > +
> > +           // Now give ownership of old storage to __guard:
> > +           __guard._M_storage = __old_start;
> > +           __guard._M_len = __capacity;
> > +           // Finally, take ownership of new storage:
> > +           this->_M_impl._M_start = __start;
> > +           this->_M_impl._M_finish = __back;
> > +           this->_M_impl._M_end_of_storage = __start + __len;
> >           }
> >         else
> >           {
> >             auto __first = ranges::begin(__rg);
> >             const auto __last = ranges::end(__rg);
> > -           for (; __first != __last; ++__first)
> > +
> > +           // Fill up to the end of current capacity.
> > +           for (auto __free = capacity() - size();
> > +                __first != __last && __free > 0;
> > +                ++__first, (void) --__free)
> >               emplace_back(*__first);
> > +
> > +           if (__first == __last)
> > +             return;
> > +
> > +           // Copy the rest of the range to a new vector.
> > +           vector __tmp(_M_get_Tp_allocator());
> > +           for (; __first != __last; ++__first)
> > +             __tmp.emplace_back(*__first);
> > +           reserve(_M_check_len(__tmp.size(), "vector::append_range"));
> > +           ranges::subrange __r(std::make_move_iterator(__tmp.begin()),
> > +                                std::make_move_iterator(__tmp.end()));
> > +           append_range(__r); // This will take the fast path above.
> >           }
> >       }
> >  #endif // ranges_to_container
> > diff --git a/libstdc++-v3/include/bits/vector.tcc 
> > b/libstdc++-v3/include/bits/vector.tcc
> > index acb2f5fca1e..f197278d52e 100644
> > --- a/libstdc++-v3/include/bits/vector.tcc
> > +++ b/libstdc++-v3/include/bits/vector.tcc
> > @@ -1094,7 +1094,7 @@ _GLIBCXX_BEGIN_NAMESPACE_CONTAINER
> >           return begin() + __ins_idx;
> >         }
> >       else
> > -       return insert_range(__pos, vector(from_range, std::move(__rg),
> > +       return insert_range(__pos, vector(from_range, 
> > std::forward<_Rg>(__rg),
> >                                           _M_get_Tp_allocator()));
> >        }
> >  #endif // ranges_to_container
> > diff --git 
> > a/libstdc++-v3/testsuite/23_containers/vector/bool/modifiers/insert/append_range.cc
> >  
> > b/libstdc++-v3/testsuite/23_containers/vector/bool/modifiers/insert/append_range.cc
> > index a35ed0f2026..bb103a47202 100644
> > --- 
> > a/libstdc++-v3/testsuite/23_containers/vector/bool/modifiers/insert/append_range.cc
> > +++ 
> > b/libstdc++-v3/testsuite/23_containers/vector/bool/modifiers/insert/append_range.cc
> > @@ -83,6 +83,56 @@ test_constexpr()
> >  {
> >    // XXX: this doesn't test the non-forward_range code paths are constexpr.
>
> I think we could remove this comment now?

The do_test call immediately below the comment still doesn't test the
non-forward paths.

The new additions do test it, although not as thoroughly as do_test
does. The new additions only check that the calls to append_range with
overlapping ranges don't hit UB (and specifically, UB that G++
actually diagnoses), they don't check the values in the resulting
range are correct. I could make the comments more clear, or add
another comment below saying that the rest of test_constexpr only
tests a subset of the functionality.

>
> >    do_test<std::span<short>, std::allocator<bool>>();
> > +
> > +  using I = std::vector<bool>::iterator;
> > +  std::vector<bool> vec(5);
> > +
> > +  struct InputRange
> > +  {
> > +
> > +    struct Sent { I end; };
> > +
> > +    struct Iter
> > +    {
> > +      using value_type = bool;
> > +      using difference_type = int;
> > +      constexpr explicit Iter(I i) : i(i) { }
> > +      constexpr Iter& operator++() { ++i; return *this; }
> > +      constexpr Iter operator++(int) { auto i = *this; ++i; return i; }
> > +      constexpr int operator*() const { return *i; }
> > +      constexpr bool operator==(const Iter&) const = default;
> > +      constexpr bool operator==(const Sent& s) const { return i == s.end; }
> > +      I i;
> > +    };
> > +
> > +    Iter iter;
> > +    Sent sent;
> > +
> > +    constexpr InputRange(I f, I l) : iter{f}, sent{l} { }
> > +    constexpr Iter begin() const { return iter; }
> > +    constexpr Sent end() const { return sent; }
> > +  };
> > +  static_assert( std::ranges::input_range<InputRange> );
> > +  static_assert( ! std::ranges::forward_range<InputRange> );
> > +
> > +  // Test input ranges
> > +  vec.resize(vec.capacity());
> > +  vec.append_range(InputRange(vec.begin(), vec.begin() + 3)); // no 
> > capacity
> > +  vec.reserve(vec.capacity() + 2);
> > +  vec.append_range(InputRange(vec.begin(), vec.begin() + 4)); // some 
> > capacity
> > +  vec.reserve(vec.capacity() + 6);
> > +  vec.append_range(InputRange(vec.begin(), vec.begin() + 5)); // enough 
> > capacity
> > +
> > +  using R = std::ranges::subrange<I>;
> > +
> > +  // Test forward ranges
> > +  vec.resize(vec.capacity());
> > +  vec.append_range(R(vec.begin(), vec.begin() + 3)); // no capacity
> > +  vec.reserve(vec.size() + 2);
> > +  vec.append_range(R(vec.begin(), vec.begin() + 4)); // some capacity
> > +  vec.reserve(vec.size() + 6);
> > +  vec.append_range(R(vec.begin(), vec.begin() + 5)); // enough capacity
> > +
> >    return true;
> >  }
> >
> > diff --git 
> > a/libstdc++-v3/testsuite/23_containers/vector/modifiers/append_range.cc 
> > b/libstdc++-v3/testsuite/23_containers/vector/modifiers/append_range.cc
> > index 5725cd2ad48..6bfe2047d1c 100644
> > --- a/libstdc++-v3/testsuite/23_containers/vector/modifiers/append_range.cc
> > +++ b/libstdc++-v3/testsuite/23_containers/vector/modifiers/append_range.cc
> > @@ -82,16 +82,180 @@ test_ranges()
> >    return true;
> >  }
> >
> > +void
> > +test_overlapping()
> > +{
> > +  using __gnu_test::test_input_range;
> > +  using __gnu_test::test_forward_range;
> > +
> > +  struct X {
> > +    unsigned* p;
> > +    constexpr X(int i = 0) : p(new unsigned(i)) { }
> > +    constexpr X(const X& m) : p(new unsigned(*m.p)) { }
> > +    constexpr X(X&& m) noexcept : p(m.p) { m.p = nullptr; }
> > +    constexpr ~X() { delete p; }
> > +  };
> > +
> > +  std::vector<X> vec;
> > +  unsigned size = 5;
> > +  vec.reserve(size);
> > +  for (unsigned i = 0; i < size; ++i)
> > +    vec.emplace_back(i);
> > +
> > +  // Append an input range that overlaps with vec.
> > +  {
> > +    __gnu_test::test_input_range<X> r(vec.data(), vec.data() + size);
> > +    vec.append_range(r);
> > +    VERIFY( vec.size() == 2 * size );
> > +    for (unsigned i = 0; i < size; ++i)
> > +    {
> > +      VERIFY( *vec[i].p == i );
> > +      VERIFY( *vec[i+size].p == i );
> > +    }
> > +  }
> > +
> > +  size = vec.size() - 2;
> > +  vec.resize(size);
> > +  for (unsigned i = 0; i < size; ++i)
> > +    *vec[i].p = i;
> > +
> > +  // Repeat with unused capacity in the vector.
> > +  {
> > +    __gnu_test::test_input_range<X> r(vec.data(), vec.data() + size);
> > +    vec.append_range(r);
> > +    VERIFY( vec.size() == 2 * size );
> > +    for (unsigned i = 0; i < size; ++i)
> > +    {
> > +      VERIFY( *vec[i].p == i );
> > +      VERIFY( *vec[i+size].p == i );
> > +    }
> > +  }
> > +
> > +  size = vec.size() - 2;
> > +  vec.resize(size);
> > +  for (unsigned i = 0; i < size; ++i)
> > +    *vec[i].p = i;
> > +
> > +  // Repeat with input range that doesn't overlap full vector.
> > +  {
> > +    __gnu_test::test_input_range<X> r(vec.data() + 1, vec.data() + 4);
> > +    vec.append_range(r);
> > +    VERIFY( vec.size() == size + 3 );
> > +    for (unsigned i = 0; i < size; ++i)
> > +    {
> > +      VERIFY( *vec[i].p == i );
> > +      if (i < 3)
> > +     VERIFY( *vec[i+size].p == i+1 );
> > +    }
> > +  }
> > +
> > +  size = 5;
> > +  vec.resize(size);
> > +  for (unsigned i = 0; i < size; ++i)
> > +    *vec[i].p = i;
> > +
> > +  // Append a forward range that overlaps with vec.
> > +  {
> > +    __gnu_test::test_forward_range<X> r(vec.data(), vec.data() + size);
> > +    vec.append_range(r);
> > +    VERIFY( vec.size() == 2 * size );
> > +    for (unsigned i = 0; i < size; ++i)
> > +    {
> > +      VERIFY( *vec[i].p == i );
> > +      VERIFY( *vec[i+size].p == i );
> > +    }
> > +  }
> > +
> > +  size = vec.size() - 2;
> > +  vec.resize(size);
> > +  for (unsigned i = 0; i < size; ++i)
> > +    *vec[i].p = i;
> > +
> > +  // Repeat with insufficient unused capacity in the vector.
> > +  {
> > +    __gnu_test::test_forward_range<X> r(vec.data(), vec.data() + size);
> > +    vec.append_range(r);
> > +    VERIFY( vec.size() == 2 * size );
> > +    for (unsigned i = 0; i < size; ++i)
> > +    {
> > +      VERIFY( *vec[i].p == i );
> > +      VERIFY( *vec[i+size].p == i );
> > +    }
> > +  }
> > +
> > +  size = vec.size() / 2;
> > +  vec.resize(size);
> > +
> > +  // Repeat with sufficient unused capacity in the vector.
> > +  {
> > +    __gnu_test::test_forward_range<X> r(vec.data(), vec.data() + size);
> > +    vec.append_range(r);
> > +    VERIFY( vec.size() == 2 * size );
> > +    for (unsigned i = 0; i < size; ++i)
> > +    {
> > +      VERIFY( *vec[i].p == i );
> > +      VERIFY( *vec[i+size].p == i );
> > +    }
> > +  }
> > +}
> > +
> >  constexpr bool
> >  test_constexpr()
> >  {
> >    // XXX: this doesn't test the non-forward_range code paths are constexpr.
>
> Same
>
> >    do_test<std::span<short>, std::allocator<int>>();
> > +
> > +  std::vector<int> vec(5);
> > +
> > +  struct InputRange
> > +  {
> > +    struct Sent { const void* end; };
> > +
> > +    struct Iter
> > +    {
> > +      using value_type = int;
> > +      using difference_type = int;
> > +      constexpr explicit Iter(int* p) : ptr(p) { }
> > +      constexpr Iter& operator++() { ++ptr; return *this; }
> > +      constexpr Iter operator++(int) { auto i = *this; ++ptr; return i; }
> > +      constexpr int operator*() const { return *ptr; }
> > +      constexpr bool operator==(const Iter&) const = default;
> > +      constexpr bool operator==(const Sent& s) const { return ptr == 
> > s.end; }
> > +      int* ptr;
> > +    };
> > +
> > +    Iter iter;
> > +    Sent sent;
> > +
> > +    constexpr InputRange(int* f, int* l) : iter{f}, sent{l} { }
> > +    constexpr Iter begin() const { return iter; }
> > +    constexpr Sent end() const { return sent; }
> > +  };
> > +  static_assert( std::ranges::input_range<InputRange> );
> > +  static_assert( ! std::ranges::forward_range<InputRange> );
> > +
> > +  // Test input ranges
> > +  vec.resize(vec.capacity());
> > +  vec.append_range(InputRange(vec.data(), vec.data() + 3)); // no capacity
> > +  vec.reserve(vec.capacity() + 2);
> > +  vec.append_range(InputRange(vec.data(), vec.data() + 4)); // some 
> > capacity
> > +  vec.reserve(vec.capacity() + 6);
> > +  vec.append_range(InputRange(vec.data(), vec.data() + 5)); // enough 
> > capacity
> > +
> > +  // Test forward ranges
> > +  vec.resize(vec.capacity());
> > +  vec.append_range(std::span<int>(vec));               // no capacity
> > +  vec.reserve(vec.size() + 2);
> > +  vec.append_range(std::span<int>(vec).subspan(1, 4)); // some capacity
> > +  vec.reserve(vec.size() + 6);
> > +  vec.append_range(std::span<int>(vec).subspan(1, 5)); // enough capacity
> > +
> >    return true;
> >  }
> >
> >  int main()
> >  {
> > +  test_overlapping();
> >    test_ranges();
> >    static_assert( test_constexpr() );
> >  }
> > --
> > 2.49.0
> >
> >
>

Reply via email to