On Thu, 28 Nov 2024 at 18:59, Jonathan Wakely <jwak...@redhat.com> wrote: > > The standard says that it's undefined to swap two containers if the > allocators are not equal and do not propagate. This ensures that swap is > always O(1) and non-throwing, but has other undesirable consequences > such as LWG 2152. The 2016 paper P0178 ("Allocators and swap") proposed > making the non-member swap handle non-equal allocators, by performing an > O(n) deep copy when needed. This ensures that a.swap(b) is still O(1) > and non-throwing, but swap(a, b) is valid for all values of the type. > > This change implements that for std::basic_stacktrace. The member swap > is changed so that for the undefined case (where we can't swap the > allocators, but can't swap storage separately from the allocators) we > just return without changing either object. This ensures that with > assertions disabled we don't separate allocated storage from the > allocator that can free it. > > For the non-member swap, perform deep copies of the two ranges, avoiding > reallocation if there is already sufficient capacity. > > libstdc++-v3/ChangeLog: > > * include/std/stacktrace (basic_stacktrace::swap): Refactor so > that the undefined case is a no-op when assertions are disabled. > (swap): Remove precondition and perform deep copies when member > swap would be undefined. > * testsuite/19_diagnostics/stacktrace/stacktrace.cc: Check > swapping with unequal, non-propagating allocators. > --- > > As part of my ongoing quest to reduce the undefined behaviour surface in > the library, this helps to avoid UB when swapping stacktrace objects. > > This is an RFC to see if people like the idea. If we do it here, we > could do it for other containers too. > > For the common case there should be no additional cost, because the > 'if constexpr' conditions will be true and swap(a, b) will just call > a.swap(b) unconditionally, which will swap the contents unconditionally. > We only do extra work in the cases that are currently undefined. > > Tested x86_64-linux. > > libstdc++-v3/include/std/stacktrace | 77 ++++++++++++++++--- > .../19_diagnostics/stacktrace/stacktrace.cc | 23 ++++++ > 2 files changed, 90 insertions(+), 10 deletions(-) > > diff --git a/libstdc++-v3/include/std/stacktrace > b/libstdc++-v3/include/std/stacktrace > index f94a424e4cf..ab0788cde08 100644 > --- a/libstdc++-v3/include/std/stacktrace > +++ b/libstdc++-v3/include/std/stacktrace > @@ -476,15 +476,79 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION > } > > // [stacktrace.basic.mod], modifiers > + > + /** Exchange the contents of two stacktrace objects > + * > + * @pre The allocators must propagate on swap or must be equal. > + */ > void > swap(basic_stacktrace& __other) noexcept > { > - std::swap(_M_impl, __other._M_impl); > if constexpr (_AllocTraits::propagate_on_container_swap::value) > - std::swap(_M_alloc, __other._M_alloc); > + { > + using std::swap; > + swap(_M_alloc, __other._M_alloc); > + } > else if constexpr (!_AllocTraits::is_always_equal::value) > { > - __glibcxx_assert(_M_alloc == __other._M_alloc); > + if (_M_alloc != __other._M_alloc) > + { > + __glibcxx_assert(_M_alloc == __other._M_alloc); > + // If assertions are disabled but the allocators are unequal, > + // we can't swap pointers, so just erroneously return. > + return; > + } > + } > + std::swap(_M_impl, __other._M_impl); > + } > + > + // [stacktrace.basic.nonmem], non-member functions > + > + /** Exchange the contents of two stacktrace objects > + * > + * Unlike the `swap` member function, this can be used with unequal > + * and non-propagating allocators. If the storage cannot be efficiently > + * swapped then the stacktrace_entry elements will be exchanged > + * one-by-one, reallocating if needed. > + */ > + friend void > + swap(basic_stacktrace& __a, basic_stacktrace& __b) > + noexcept(_AllocTraits::propagate_on_container_swap::value > + || _AllocTraits::is_always_equal::value) > + { > + if constexpr (_AllocTraits::propagate_on_container_swap::value > + || _AllocTraits::is_always_equal::value) > + __a.swap(__b); > + else if (__a._M_alloc == __b._M_alloc) [[likely]] > + __a.swap(__b); > + else // O(N) swap for non-equal non-propagating allocators > + { > + basic_stacktrace* __p[2]{ std::__addressof(__a), > + std::__addressof(__b) }; > + if (__p[0]->size() > __p[1]->size()) > + std::swap(__p[0], __p[1]); > + basic_stacktrace& __a = *__p[0]; // shorter sequence > + basic_stacktrace& __b = *__p[1]; // longer sequence > + > + const auto __a_sz = __a.size(); > + auto __a_begin = __a._M_impl._M_frames; > + auto __a_end = __a._M_impl._M_frames + __a_sz; > + auto __b_begin = __b._M_impl._M_frames; > + > + if (__a._M_impl._M_capacity < __b.size()) > + { > + // Reallocation needed. > + basic_stacktrace __tmp(__b, __a._M_alloc); > + std::copy(__a_begin, __a_end, __b_begin); > + __b._M_impl._M_resize(__a_sz, __b._M_alloc); > + std::swap(__tmp._M_impl, __a._M_impl); > + return; > + } > + > + // Exchange contents in-place. > + auto __mid = std::swap_ranges(__a_begin, __a_end, __b_begin); > + std::__relocate_a(__mid, __b_begin + __a_sz, __a_end, > __a._M_alloc);
Oops, this should be __b_begin + __b.size() for the second argument. > + std::swap(__a._M_impl._M_size, __b._M_impl._M_size); > } > } > > @@ -659,13 +723,6 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION > // basic_stacktrace typedef names > using stacktrace = basic_stacktrace<allocator<stacktrace_entry>>; > > - // [stacktrace.basic.nonmem], non-member functions > - template<typename _Allocator> > - inline void > - swap(basic_stacktrace<_Allocator>& __a, basic_stacktrace<_Allocator>& > __b) > - noexcept(noexcept(__a.swap(__b))) > - { __a.swap(__b); } > - > inline ostream& > operator<<(ostream& __os, const stacktrace_entry& __f) > { > diff --git a/libstdc++-v3/testsuite/19_diagnostics/stacktrace/stacktrace.cc > b/libstdc++-v3/testsuite/19_diagnostics/stacktrace/stacktrace.cc > index ee1a6d221e3..8e784071b29 100644 > --- a/libstdc++-v3/testsuite/19_diagnostics/stacktrace/stacktrace.cc > +++ b/libstdc++-v3/testsuite/19_diagnostics/stacktrace/stacktrace.cc > @@ -393,6 +393,29 @@ test_swap() > VERIFY( s1.get_allocator().get_personality() == 2 ); > VERIFY( s2.get_allocator().get_personality() == 1 ); > } > + > + { > + using Alloc > + = __gnu_test::propagating_allocator<std::stacktrace_entry, false>; > + using Stacktrace = std::basic_stacktrace<Alloc>; > + > + Stacktrace s1 = Stacktrace::current(Alloc{1}); > + Stacktrace s2(Alloc{2}); > + const Stacktrace s3 = s1; > + swap(s1, s2); > + VERIFY( s1.empty() ); > + VERIFY( s2 == s3 ); > + VERIFY( s1.get_allocator().get_personality() == 1 ); > + VERIFY( s2.get_allocator().get_personality() == 2 ); > + > + s1 = s3; > + s2 = Stacktrace(); > + swap(s1, s2); > + VERIFY( s1.empty() ); > + VERIFY( s2 == s3 ); > + VERIFY( s1.get_allocator().get_personality() == 1 ); > + VERIFY( s2.get_allocator().get_personality() == 2 ); > + } > } > > void > -- > 2.47.0 >