From f19b8d50bf6653214a0a68e62af12f1de6f65de3 Mon Sep 17 00:00:00 2001 From: Felix Morgner Date: Fri, 2 Oct 2026 19:33:32 +0200 Subject: kstd: ring_buffer: implement swap --- libs/kstd/kstd/ring_buffer.hpp | 35 +++++ libs/kstd/kstd/ring_buffer.tests.cpp | 250 +++++++++++++++++++++++++++++++++++ 2 files changed, 285 insertions(+) diff --git a/libs/kstd/kstd/ring_buffer.hpp b/libs/kstd/kstd/ring_buffer.hpp index 7b9012aa..152bf697 100644 --- a/libs/kstd/kstd/ring_buffer.hpp +++ b/libs/kstd/kstd/ring_buffer.hpp @@ -774,6 +774,41 @@ namespace kstd //! @} + //! @name Swap + //! @{ + + //! Swap the contents of this ring buffer with another. + //! + //! @param other The other ring buffer to swap with. + constexpr auto friend swap(ring_buffer & lhs, + ring_buffer & rhs) noexcept(std::is_nothrow_swappable_v && + std::is_nothrow_move_constructible_v) -> void + requires(Capacity > 0) + { + auto const to_swap = std::min(lhs.m_size, rhs.m_size); + std::ranges::for_each(std::views::iota(0uz, to_swap), + [&](auto i) { std::ranges::swap(*lhs.element_at(i), *rhs.element_at(i)); }); + + if (rhs.m_size > lhs.m_size) + { + std::ranges::for_each(std::views::iota(to_swap, rhs.m_size), [&](auto i) { + std::construct_at(lhs.element_at(i), std::move(*rhs.element_at(i))); + std::destroy_at(rhs.element_at(i)); + }); + } + else if (lhs.m_size > to_swap) + { + std::ranges::for_each(std::views::iota(to_swap, lhs.m_size), [&](auto i) { + std::construct_at(rhs.element_at(i), std::move(*lhs.element_at(i))); + std::destroy_at(lhs.element_at(i)); + }); + } + + std::ranges::swap(lhs.m_size, rhs.m_size); + } + + //! @} + private: //! Get a pointer to the element at the given read-index relative position. //! diff --git a/libs/kstd/kstd/ring_buffer.tests.cpp b/libs/kstd/kstd/ring_buffer.tests.cpp index b08ee222..d870c2f8 100644 --- a/libs/kstd/kstd/ring_buffer.tests.cpp +++ b/libs/kstd/kstd/ring_buffer.tests.cpp @@ -253,6 +253,11 @@ SCENARIO("Ring Buffer interface types", "[kstd][ring_buffer]") { STATIC_REQUIRE(std::is_same_v().emplace_back(0.0f))>); } + + THEN("the return type of swap() is void") + { + STATIC_REQUIRE(std::is_same_v(), std::declval()))>); + } } } @@ -2094,4 +2099,249 @@ SCENARIO("Ring Buffer comparison", "[kstd][ring_buffer]") REQUIRE(lhs == rhs); } } +} + +SCENARIO("Ring Buffer swap", "[kstd][ring_buffer]") +{ + GIVEN("Two ring buffers of equal size, both freshly constructed") + { + auto a = kstd::ring_buffer{3, 1}; + auto b = kstd::ring_buffer{3, 2}; + + WHEN("swapping them") + { + swap(a, b); + + THEN("their sizes are unchanged") + { + REQUIRE(a.size() == 3); + REQUIRE(b.size() == 3); + } + + THEN("their contents are exchanged") + { + REQUIRE_THAT(a, Catch::Matchers::RangeEquals({2, 2, 2})); + REQUIRE_THAT(b, Catch::Matchers::RangeEquals({1, 1, 1})); + } + } + } + + GIVEN("Two ring buffers with the same logical content but different physical layouts") + { + auto fresh = kstd::ring_buffer{}; + fresh.push_back(10); + fresh.push_back(20); + fresh.push_back(30); + + auto wrapped = kstd::ring_buffer{}; + for (int v : {1, 2, 3, 4, 5}) + { + wrapped.push_back(v); + } + wrapped.pop_front(); + wrapped.pop_front(); + + WHEN("swapping them") + { + swap(fresh, wrapped); + + THEN("each buffer ends up holding the other's logical content") + { + REQUIRE_THAT(fresh, Catch::Matchers::RangeEquals({3, 4, 5})); + REQUIRE_THAT(wrapped, Catch::Matchers::RangeEquals({10, 20, 30})); + } + } + } + + GIVEN("A smaller and a larger ring buffer") + { + auto small = kstd::ring_buffer{2, 7}; + auto large = kstd::ring_buffer{5, 100}; + + WHEN("swapping with the smaller buffer passed first") + { + swap(small, large); + + THEN("the sizes are exchanged") + { + REQUIRE(small.size() == 5); + REQUIRE(large.size() == 2); + } + + THEN("the contents are exchanged") + { + REQUIRE_THAT(small, Catch::Matchers::RangeEquals({100, 100, 100, 100, 100})); + REQUIRE_THAT(large, Catch::Matchers::RangeEquals({7, 7})); + } + } + + WHEN("swapping with the larger buffer passed first") + { + swap(large, small); + + THEN("the sizes are exchanged") + { + REQUIRE(large.size() == 2); + REQUIRE(small.size() == 5); + } + + THEN("the contents are exchanged") + { + REQUIRE_THAT(large, Catch::Matchers::RangeEquals({7, 7})); + REQUIRE_THAT(small, Catch::Matchers::RangeEquals({100, 100, 100, 100, 100})); + } + } + } + + GIVEN("Two ring buffers that differ in both size and physical layout") + { + auto a = kstd::ring_buffer{}; + for (int v : {1, 2, 3, 4, 5, 6}) + { + a.push_back(v); + } + a.pop_front(); + + auto b = kstd::ring_buffer{}; + for (int v : {10, 20, 30, 40, 50}) + { + b.push_back(v); + } + b.pop_front(); + b.pop_front(); + b.pop_front(); + + WHEN("swapping them") + { + swap(a, b); + + THEN("the sizes are exchanged") + { + REQUIRE(a.size() == 2); + REQUIRE(b.size() == 4); + } + + THEN("the contents are exchanged") + { + REQUIRE_THAT(a, Catch::Matchers::RangeEquals({40, 50})); + REQUIRE_THAT(b, Catch::Matchers::RangeEquals({3, 4, 5, 6})); + } + } + } + + GIVEN("One empty and one non-empty ring buffer") + { + auto empty = kstd::ring_buffer{}; + auto full = kstd::ring_buffer{3, 10}; + + WHEN("swapping them") + { + swap(empty, full); + + THEN("the previously empty buffer now holds the content") + { + REQUIRE_THAT(empty, Catch::Matchers::RangeEquals({10, 10, 10})); + } + + THEN("the previously full buffer is now empty") + { + REQUIRE(full.empty()); + } + } + } + + GIVEN("Two empty ring buffers") + { + auto a = kstd::ring_buffer{}; + auto b = kstd::ring_buffer{}; + + WHEN("swapping them") + { + swap(a, b); + + THEN("both remain empty") + { + REQUIRE(a.empty()); + REQUIRE(b.empty()); + } + } + } + + GIVEN("A ring buffer swapped with itself") + { + auto buffer = kstd::ring_buffer{3, 1}; + buffer.at(1) = 2; + buffer.at(2) = 3; + + WHEN("swapping it with itself") + { + swap(buffer, buffer); + + THEN("its content is unchanged") + { + REQUIRE_THAT(buffer, Catch::Matchers::RangeEquals({1, 2, 3})); + } + } + } + + GIVEN("Two partially filled ring buffers of static operation trackers, equal size") + { + auto a = kstd::ring_buffer{3}; + auto b = kstd::ring_buffer{3}; + + WHEN("swapping them") + { + kstd::tests::static_copy_move_tracker::reset(); + swap(a, b); + + THEN("each element is exchanged via one move construction and two move assignments, nothing is leaked") + { + REQUIRE(kstd::tests::static_copy_move_tracker::move_ctor_call_count == 3); + REQUIRE(kstd::tests::static_copy_move_tracker::move_assignment_call_count == 6); + REQUIRE(kstd::tests::static_copy_move_tracker::dtor_call_count == 3); + REQUIRE(kstd::tests::static_copy_move_tracker::copy_ctor_call_count == 0); + REQUIRE(kstd::tests::static_copy_move_tracker::copy_assignment_call_count == 0); + } + } + } + + GIVEN("Two ring buffers of static operation trackers with different sizes") + { + auto large = kstd::ring_buffer{5}; + auto small = kstd::ring_buffer{2}; + + WHEN("swapping them") + { + kstd::tests::static_copy_move_tracker::reset(); + swap(large, small); + + THEN("the shared prefix is exchanged by swapping, the remainder by move-construction, nothing is leaked") + { + REQUIRE(kstd::tests::static_copy_move_tracker::move_ctor_call_count == 5); + REQUIRE(kstd::tests::static_copy_move_tracker::move_assignment_call_count == 4); + REQUIRE(kstd::tests::static_copy_move_tracker::dtor_call_count == 5); + REQUIRE(kstd::tests::static_copy_move_tracker::copy_ctor_call_count == 0); + REQUIRE(kstd::tests::static_copy_move_tracker::copy_assignment_call_count == 0); + } + } + } + + GIVEN("A value type whose move operations are noexcept") + { + THEN("swap() is noexcept") + { + STATIC_REQUIRE( + noexcept(swap(std::declval &>(), std::declval &>()))); + } + } + + GIVEN("A value type whose move operations are not noexcept") + { + THEN("swap() is not noexcept") + { + STATIC_REQUIRE_FALSE( + noexcept(swap(std::declval &>(), + std::declval &>()))); + } + } } \ No newline at end of file -- cgit v1.2.3