From 37f32d7b1d97208234bc22e3a1fe5ab31f246d0d Mon Sep 17 00:00:00 2001 From: rootkiller6788 Date: Fri, 21 Aug 2026 21:39:48 +0800 Subject: [PATCH] Fix circular_queue pop_front(count) debug assert and empty() pop_front(count) used a wrong debug assumption: it asserted that offset_ wraps to zero exactly when count == chunk, but offset_ only wraps when the first chunk reaches the end of the buffer. Any valid pop_front(count) that does not wrap (or that pops more than one chunk) aborted the process in debug builds. The assertion now checks the actual safety precondition: when more elements remain beyond the first chunk, the buffer must have wrapped around. empty() reported the backing vector's emptiness instead of the queue's logical emptiness, so a drained queue (size() == 0) still reported empty() == false, breaking the while (!q.empty()) pop_front() idiom (and enabling front()/back() to read destroyed elements). It now returns size_ == 0. Add regression tests for pop_front(count) with and without wrap-around and for empty() after draining. --- fatal/container/circular_queue.h | 12 ++-- fatal/container/test/circular_queue_test.cpp | 63 ++++++++++++++++++++ 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/fatal/container/circular_queue.h b/fatal/container/circular_queue.h index 7542df97..49d98610 100644 --- a/fatal/container/circular_queue.h +++ b/fatal/container/circular_queue.h @@ -259,7 +259,9 @@ class circular_queue { FATAL_ASSUME_LT(offset_, queue_.size()); FATAL_ASSUME_LE(chunk, count); - FATAL_ASSUME_EQ(offset_ == 0, count == chunk); + // when there are more elements to destroy beyond the first chunk, the + // first chunk must have wrapped around the buffer (resetting offset_) + FATAL_ASSUME_IF(FATAL_GT(count, chunk), FATAL_IS_TRUE(offset_ == 0)); for (auto const end = count - chunk; offset_ < end; ++offset_) { queue_[offset_].value.~value_type(); } @@ -475,13 +477,7 @@ class circular_queue { fatal::fast_pass size() const noexcept { return size_; } - bool empty() const noexcept { - static_assert( - noexcept(queue_.empty()), - "underlying container must provide a noexcept empty()" - ); - return queue_.empty(); - } + bool empty() const noexcept { return size_ == 0; } using const_iterator = random_access_iterator; using iterator = random_access_iterator; diff --git a/fatal/container/test/circular_queue_test.cpp b/fatal/container/test/circular_queue_test.cpp index 7254cb8d..33c88718 100644 --- a/fatal/container/test/circular_queue_test.cpp +++ b/fatal/container/test/circular_queue_test.cpp @@ -306,6 +306,69 @@ FATAL_TEST(circular_queue, shift_to_back_by) { } } +FATAL_TEST(circular_queue, pop_front_count) { + circular_queue q; + for (int i = 0; i < 10; ++i) { + q.push_back(i); + } + CHECK_CONTENTS(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); + + // popping a prefix that does not wrap around the buffer + q.pop_front(3); + CHECK_CONTENTS(3, 4, 5, 6, 7, 8, 9); + + // popping zero elements is a no-op + q.pop_front(0); + CHECK_CONTENTS(3, 4, 5, 6, 7, 8, 9); + + // popping the remainder drains the queue + q.pop_front(7); + CHECK_CONTENTS(); + + FATAL_EXPECT_TRUE(q.empty()); +} + +FATAL_TEST(circular_queue, pop_front_count_wrap) { + circular_queue q; + for (int i = 0; i < 10; ++i) { + q.push_back(i); + } + + // drain the queue so the elements wrap around the buffer on the next pushes + q.pop_front(10); + for (int i = 0; i < 8; ++i) { + q.push_back(i); + } + CHECK_CONTENTS(0, 1, 2, 3, 4, 5, 6, 7); + + // the first chunk does not reach the end of the buffer + q.pop_front(5); + CHECK_CONTENTS(5, 6, 7); + + // the first chunk reaches the end of the buffer and the remainder wraps + q.pop_front(3); + CHECK_CONTENTS(); + + FATAL_EXPECT_TRUE(q.empty()); +} + +FATAL_TEST(circular_queue, empty_after_drain) { + circular_queue q; + FATAL_EXPECT_TRUE(q.empty()); + + for (int i = 0; i < 10; ++i) { + q.push_back(i); + FATAL_EXPECT_FALSE(q.empty()); + } + + while (!q.empty()) { + q.pop_front(); + } + + FATAL_EXPECT_EQ(0, q.size()); + FATAL_EXPECT_TRUE(q.empty()); +} + # undef CHECK_CONTENTS template