Add push_front and emplace_front to absl::chunked_queue. This implements push_front and emplace_front operations, prepending elements into available space in the head block or allocating a new head block when needed. Corresponding unit tests, exception safety tests, fuzzing actions, and benchmarks are also added. PiperOrigin-RevId: 998673257 Change-Id: Ibe23e6276d1b016128c2505c756acb59ae9688ac
diff --git a/absl/container/BUILD.bazel b/absl/container/BUILD.bazel index d325e39..850eaec 100644 --- a/absl/container/BUILD.bazel +++ b/absl/container/BUILD.bazel
@@ -1488,6 +1488,7 @@ "//absl/base:core_headers", "//absl/base:hardening", "//absl/base:iterator_traits_internal", + "//absl/cleanup", ], )
diff --git a/absl/container/CMakeLists.txt b/absl/container/CMakeLists.txt index 79bcee0..7a2641c 100644 --- a/absl/container/CMakeLists.txt +++ b/absl/container/CMakeLists.txt
@@ -1239,6 +1239,7 @@ LINKOPTS ${ABSL_DEFAULT_LINKOPTS} DEPS + absl::cleanup absl::config absl::core_headers absl::hardening
diff --git a/absl/container/chunked_queue.h b/absl/container/chunked_queue.h index bcaa508..54cb6d8 100644 --- a/absl/container/chunked_queue.h +++ b/absl/container/chunked_queue.h
@@ -35,6 +35,8 @@ // shrink_to_fit() // resize() // assign() +// push_front() +// emplace_front() // push_back() // emplace_back() // pop_front() @@ -110,6 +112,7 @@ #include "absl/base/internal/hardening.h" #include "absl/base/internal/iterator_traits.h" #include "absl/base/macros.h" +#include "absl/cleanup/cleanup.h" #include "absl/container/internal/chunked_queue.h" #include "absl/container/internal/layout.h" @@ -402,6 +405,20 @@ // Replaces the contents with the elements from the initializer list `il`. void assign(std::initializer_list<T> il) { assign(il.begin(), il.end()); } + // Prepends the given element value to the beginning of the container. + // Invalidates `end()` iterator if the container was empty. References to + // other elements remain valid. + void push_front(const T& val) { emplace_front(val); } + void push_front(T&& val) { emplace_front(std::move(val)); } + + // Prepends a new element to the beginning of the container. + // The element is constructed in-place with `args`. + // Returns a reference to the new element. + // Invalidates `end()` iterator if the container was empty. References to + // other elements remain valid. + template <typename... A> + T& emplace_front(A&&... args); + // Appends the given element value to the end of the container. // Invalidates `end()` iterator. References to other elements remain valid. void push_back(const T& val) { emplace_back(val); } @@ -537,10 +554,13 @@ } void AddTailBlock(); - size_t NewBlockSize() { - // Double the last block size and bound to [kBlockSizeMin, kBlockSizeMax]. - if (!tail_.block) return kBlockSizeMin; - return (std::min)(kBlockSizeMax, 2 * tail_.block->size()); + // Returns the capacity (in elements) to allocate for a new block adjacent to + // `b` (`head_.block` when prepending or `tail_.block` when appending). Starts + // at `kBlockSizeMin` when `b` is null, otherwise doubles `b->size()` and + // clamps to `[kBlockSizeMin, kBlockSizeMax]`. + size_t NewBlockSize(Block* b) { + if (!b) return kBlockSizeMin; + return (std::min)(kBlockSizeMax, 2 * b->size()); } T* AllocateBack(); @@ -651,9 +671,70 @@ } template <typename T, size_t BLo, size_t BHi, typename Allocator> +template <typename... A> +T& chunked_queue<T, BLo, BHi, Allocator>::emplace_front(A&&... args) { + // Per "The set of elements in the queue" / "Rep invariants" (documented above + // `head_` and `tail_`), in a multi-block queue the head block holds + // `[head_.ptr .. head_.block->limit()-1]`, interior blocks hold all elements + // `[b->start() .. b->limit()-1]`, and the tail block holds + // `[tail_.block->start() .. tail_.ptr-1]` (as also assumed by `block_begin()` + // and `iterator_common::Incr()`). Therefore, a head block is populated from + // `b->limit() - 1` down to `b->start()` before a new head block can be + // prepended. + + // Determine the target block `b` and element slot `storage`, allocating a new + // head block if needed. + Block* b = head_.block; + T* storage; + if (empty()) { + // Empty queue: allocate the first block if none exists, or reuse the empty + // block retained by `clear()` / `pop_front()`. + if (!b) { + b = Block::New(NewBlockSize(b), &alloc_and_size_.allocator()); + } + storage = b->limit() - 1; + } else if (head_.ptr == head_.block->start()) { + // Full head block (no spare capacity before `head_.ptr`): allocate a new + // head block to prepend. + b = Block::New(NewBlockSize(b), &alloc_and_size_.allocator()); + storage = b->limit() - 1; + } else { + // Partially filled head block: use the slot immediately before `head_.ptr`. + storage = head_.ptr - 1; + } + + // Construct the new element in `storage`. + { + // Exception safety: Unlike `emplace_back()` (where `AddTailBlock()` links + // the new block into `tail_` before construction with `tail_.ptr == + // b->start()`, so the container owns it if construction throws), `b` is not + // linked into `head_` until after construction succeeds so that `head_` + // remains valid on throw. Delete `b` if construction throws. + absl::Cleanup cleanup = [&] { + if (b != head_.block) Block::Delete(b, &alloc_and_size_.allocator()); + }; + AllocatorTraits::construct(alloc_and_size_.allocator(), storage, + std::forward<A>(args)...); + std::move(cleanup).Cancel(); + } + + // Link the new block (if allocated) and update queue iterators and size. + if (empty()) { + tail_.block = b; + tail_.ptr = b->limit(); + } else if (b != head_.block) { + b->set_next(head_.block); + } + head_.block = b; + head_.ptr = storage; + ++alloc_and_size_.size; + return *storage; +} + +template <typename T, size_t BLo, size_t BHi, typename Allocator> inline void chunked_queue<T, BLo, BHi, Allocator>::AddTailBlock() { ABSL_ASSERT(!tail_.block || tail_.ptr == tail_.block->limit()); - auto* b = Block::New(NewBlockSize(), &alloc_and_size_.allocator()); + auto* b = Block::New(NewBlockSize(tail_.block), &alloc_and_size_.allocator()); if (!head_.block) { ABSL_ASSERT(!tail_.block); head_ = iterator(b);
diff --git a/absl/container/chunked_queue_benchmark.cc b/absl/container/chunked_queue_benchmark.cc index ee4d3c1..3c031ed 100644 --- a/absl/container/chunked_queue_benchmark.cc +++ b/absl/container/chunked_queue_benchmark.cc
@@ -38,6 +38,16 @@ ~forward_list_queue() = default; template <typename... Args> + void emplace_front(Args&&... args) { + if (list_.empty()) { + list_.emplace_front(std::forward<Args>(args)...); + tail_ = list_.begin(); + } else { + list_.emplace_front(std::forward<Args>(args)...); + } + } + + template <typename... Args> void emplace_back(Args&&... args) { if (list_.empty()) { list_.emplace_front(std::forward<Args>(args)...); @@ -48,6 +58,7 @@ } } + void push_front(const T& value) { emplace_front(value); } void push_back(const T& value) { emplace_back(value); } iterator begin() { return list_.begin(); } iterator end() { return list_.end(); } @@ -146,6 +157,31 @@ BENCHMARK_TEMPLATE(BM_destroy, ExpChunked<Element>)->Apply(CustomArgs); template <class Q> +void BM_push_front(benchmark::State& state) { + const int64_t num_elements = state.range(0); + + state.SetItemsProcessed(state.max_iterations * num_elements); + for (auto s : state) { + state.PauseTiming(); + Q q; + state.ResumeTiming(); + for (int j = 0; j < num_elements; j++) q.push_front(j); + benchmark::DoNotOptimize(q); + } +} + +BENCHMARK_TEMPLATE(BM_push_front, Deque<int64_t>)->Apply(CustomArgs); +BENCHMARK_TEMPLATE(BM_push_front, List<int64_t>)->Apply(CustomArgs); +BENCHMARK_TEMPLATE(BM_push_front, FwdList<int64_t>)->Apply(CustomArgs); +BENCHMARK_TEMPLATE(BM_push_front, Chunked<int64_t>)->Apply(CustomArgs); +BENCHMARK_TEMPLATE(BM_push_front, ExpChunked<int64_t>)->Apply(CustomArgs); +BENCHMARK_TEMPLATE(BM_push_front, Deque<Element>)->Apply(CustomArgs); +BENCHMARK_TEMPLATE(BM_push_front, List<Element>)->Apply(CustomArgs); +BENCHMARK_TEMPLATE(BM_push_front, FwdList<Element>)->Apply(CustomArgs); +BENCHMARK_TEMPLATE(BM_push_front, Chunked<Element>)->Apply(CustomArgs); +BENCHMARK_TEMPLATE(BM_push_front, ExpChunked<Element>)->Apply(CustomArgs); + +template <class Q> void BM_push_back(benchmark::State& state) { const int64_t num_elements = state.range(0);
diff --git a/absl/container/chunked_queue_test.cc b/absl/container/chunked_queue_test.cc index f3f9b3e..b4604b6 100644 --- a/absl/container/chunked_queue_test.cc +++ b/absl/container/chunked_queue_test.cc
@@ -346,6 +346,59 @@ EXPECT_EQ(0, q.size()); } +TEST(ChunkedQueue, PushFrontAndPop) { + absl::chunked_queue<int64_t> q; + EXPECT_TRUE(q.empty()); + EXPECT_EQ(0, q.size()); + // Push enough elements to exercise exponential block growth from + // `kBlockSizeMin` (1) up to `kBlockSizeMax` (128) across multiple max-sized + // head blocks (`1 + 2 + 4 + 8 + 16 + 32 + 64 + 128 = 255`). + constexpr int kNumElements = 500; + for (int i = 0; i < kNumElements; i++) { + q.push_front(i); + EXPECT_EQ(q.front(), i) << ": iteration " << i; + EXPECT_EQ(q.back(), 0) << ": iteration " << i; + EXPECT_FALSE(q.empty()); + EXPECT_EQ(i + 1, q.size()); + } + for (int i = 0; i < kNumElements; i++) { + EXPECT_FALSE(q.empty()); + EXPECT_EQ(kNumElements - i, q.size()); + EXPECT_EQ(q.front(), kNumElements - 1 - i); + EXPECT_EQ(q.back(), 0); + q.pop_front(); + } + EXPECT_TRUE(q.empty()); + EXPECT_EQ(0, q.size()); +} + +TEST(ChunkedQueue, PushFrontAndPushBack) { + absl::chunked_queue<int64_t, 4> q; + q.push_front(2); + q.push_back(3); + q.push_front(1); + q.push_back(4); + q.push_front(0); + q.push_back(5); + EXPECT_THAT(q, ElementsAre(0, 1, 2, 3, 4, 5)); + EXPECT_EQ(q.front(), 0); + EXPECT_EQ(q.back(), 5); + + q.pop_front(); + q.push_front(-1); + EXPECT_THAT(q, ElementsAre(-1, 1, 2, 3, 4, 5)); + + q.clear(); + EXPECT_TRUE(q.empty()); + q.push_front(10); + EXPECT_THAT(q, ElementsAre(10)); + EXPECT_EQ(q.front(), 10); + EXPECT_EQ(q.back(), 10); + q.push_front(9); + q.push_back(11); + EXPECT_THAT(q, ElementsAre(9, 10, 11)); +} + TEST(ChunkedQueue, Swap) { absl::chunked_queue<int64_t> q1; absl::chunked_queue<int64_t> q2; @@ -662,6 +715,28 @@ EXPECT_EQ(*++i, 2); } +// Verifies that `push_front` on a non-empty queue does not invalidate +// iterators to existing elements, both when prepending into remaining space in +// the current head block (`1`) and when allocating a new head block (`0`), and +// that advancing `begin()` across the block boundary reaches the existing +// iterator `i`. +TEST(ChunkedQueue, Iterator_PushFrontInvalidate) { + absl::chunked_queue<int64_t, 2> q; + q.push_front(2); + auto i = q.begin(); + EXPECT_EQ(*i, 2); + q.push_front(1); + EXPECT_EQ(*i, 2); + q.push_front(0); + EXPECT_EQ(*i, 2); + auto j = q.begin(); + EXPECT_EQ(*j, 0); + EXPECT_EQ(*++j, 1); + EXPECT_EQ(*++j, 2); + EXPECT_TRUE(j == i); + EXPECT_TRUE(++j == q.end()); +} + struct MyType { static int constructor_calls; static int destructor_calls; @@ -707,13 +782,56 @@ absl::chunked_queue<std::unique_ptr<int>> q; q.push_back(std::make_unique<int>(10)); q.push_back(std::make_unique<int>(11)); + q.push_front(std::make_unique<int>(9)); + EXPECT_EQ(9, *q.front()); + q.pop_front(); EXPECT_EQ(10, *q.front()); q.pop_front(); EXPECT_EQ(11, *q.front()); q.pop_front(); } +// Verifies that `emplace_front` constructs elements in-place at the front of +// the queue, returns a reference to the newly inserted element, and preserves +// existing elements at the back. +TEST(ChunkedQueue, EmplaceFrontUpdatesFrontAndReturnsReference) { + absl::chunked_queue<std::pair<int, int>> q; + auto& v1 = q.emplace_front(1, 2); + EXPECT_THAT(v1, Pair(1, 2)); + EXPECT_THAT(q.front(), Pair(1, 2)); + EXPECT_EQ(&v1, &q.front()); + EXPECT_EQ(&v1, &q.back()); + + auto& v2 = q.emplace_front(3, 4); + EXPECT_THAT(v2, Pair(3, 4)); + EXPECT_THAT(q.front(), Pair(3, 4)); + EXPECT_THAT(q.back(), Pair(1, 2)); + EXPECT_EQ(&v2, &q.front()); + EXPECT_EQ(&v1, &q.back()); +} + +// Verifies that `emplace_front` forwards move-only arguments to construct the +// element in-place. +TEST(ChunkedQueue, EmplaceFrontMoveOnlyType) { + absl::chunked_queue<std::pair<std::unique_ptr<int>, std::string>> q; + auto& v = q.emplace_front(std::make_unique<int>(11), "val12"); + EXPECT_THAT(v, Pair(Pointee(11), "val12")); + EXPECT_THAT(q.front(), Pair(Pointee(11), "val12")); +} + +TEST(ChunkedQueue, OveralignmentEmplaceFront) { + struct alignas(64) Overaligned { + int x; + int y; + }; + absl::chunked_queue<Overaligned, 1, 8> q; + for (int i = 0; i < 10; ++i) { + auto& v = q.emplace_front(Overaligned{i, i}); + EXPECT_EQ(reinterpret_cast<uintptr_t>(&v) % 64, 0); + } +} + TEST(ChunkedQueue, EmplaceBack1) { absl::chunked_queue<std::pair<int, int>> q; auto& v = q.emplace_back(1, 2); @@ -865,4 +983,111 @@ } #endif +TEST(ChunkedQueue, StrongExceptionSafetyEmplaceFrontEmptyQueue) { +#ifndef ABSL_HAVE_EXCEPTIONS + GTEST_SKIP() << "Exceptions are not enabled."; +#else + int ctor_count = 0; + int dtor_count = 0; + absl::chunked_queue<ThrowingCtor> q; + + try { + q.emplace_front(5, true, &ctor_count, &dtor_count); + } catch (...) { + } + + EXPECT_TRUE(q.empty()); + EXPECT_EQ(ctor_count, 0); + EXPECT_EQ(dtor_count, 0); +#endif +} + +TEST(ChunkedQueue, StrongExceptionSafetyEmplaceFrontNewHeadBlock) { +#ifndef ABSL_HAVE_EXCEPTIONS + GTEST_SKIP() << "Exceptions are not enabled."; +#else + int ctor_count = 0; + int dtor_count = 0; + absl::chunked_queue<ThrowingCtor> q; + q.emplace_front(10, false, &ctor_count, &dtor_count); + ASSERT_EQ(q.size(), 1); + ASSERT_EQ(ctor_count, 1); + ASSERT_EQ(dtor_count, 0); + + // The first block has size 1, so the next emplace_front allocates a new head + // block and must delete it if element construction throws. + try { + q.emplace_front(15, true, &ctor_count, &dtor_count); + } catch (...) { + } + + EXPECT_EQ(q.size(), 1); + EXPECT_EQ(ctor_count, 1); + EXPECT_EQ(dtor_count, 0); + EXPECT_EQ(q.front().value, 10); + EXPECT_EQ(q.back().value, 10); + + q.clear(); + EXPECT_EQ(q.size(), 0); + EXPECT_EQ(dtor_count, 1); +#endif +} + +TEST(ChunkedQueue, StrongExceptionSafetyEmplaceFrontExistingHeadBlock) { +#ifndef ABSL_HAVE_EXCEPTIONS + GTEST_SKIP() << "Exceptions are not enabled."; +#else + int ctor_count = 0; + int dtor_count = 0; + absl::chunked_queue<ThrowingCtor, 2> q; + q.emplace_front(10, false, &ctor_count, &dtor_count); + ASSERT_EQ(q.size(), 1); + ASSERT_EQ(ctor_count, 1); + ASSERT_EQ(dtor_count, 0); + + // The head block has capacity 2 with 1 element, so the next emplace_front + // constructs into the remaining slot in the existing head block. + try { + q.emplace_front(20, true, &ctor_count, &dtor_count); + } catch (...) { + } + + EXPECT_EQ(q.size(), 1); + EXPECT_EQ(ctor_count, 1); + EXPECT_EQ(dtor_count, 0); + EXPECT_EQ(q.front().value, 10); + EXPECT_EQ(q.back().value, 10); + + q.clear(); + EXPECT_EQ(q.size(), 0); + EXPECT_EQ(dtor_count, 1); +#endif +} + +TEST(ChunkedQueue, StrongExceptionSafetyEmplaceFrontClearedQueue) { +#ifndef ABSL_HAVE_EXCEPTIONS + GTEST_SKIP() << "Exceptions are not enabled."; +#else + int ctor_count = 0; + int dtor_count = 0; + absl::chunked_queue<ThrowingCtor> q; + q.emplace_front(10, false, &ctor_count, &dtor_count); + q.clear(); + ASSERT_TRUE(q.empty()); + ASSERT_EQ(ctor_count, 1); + ASSERT_EQ(dtor_count, 1); + + // `clear()` leaves one empty block allocated; throwing while constructing + // into that retained block must leave the queue empty and valid. + try { + q.emplace_front(20, true, &ctor_count, &dtor_count); + } catch (...) { + } + + EXPECT_TRUE(q.empty()); + EXPECT_EQ(ctor_count, 1); + EXPECT_EQ(dtor_count, 1); +#endif +} + } // namespace