summaryrefslogtreecommitdiff
path: root/asl/handle_pool
diff options
context:
space:
mode:
Diffstat (limited to 'asl/handle_pool')
-rw-r--r--asl/handle_pool/BUILD.bazel23
-rw-r--r--asl/handle_pool/sparse_handle_pool.hpp158
-rw-r--r--asl/handle_pool/sparse_handle_pool_tests.cpp117
3 files changed, 298 insertions, 0 deletions
diff --git a/asl/handle_pool/BUILD.bazel b/asl/handle_pool/BUILD.bazel
index 08a41a9..cb3c44d 100644
--- a/asl/handle_pool/BUILD.bazel
+++ b/asl/handle_pool/BUILD.bazel
@@ -32,6 +32,17 @@ cc_library(
visibility = ["//visibility:public"],
)
+cc_library(
+ name = "sparse_handle_pool",
+ hdrs = [
+ "sparse_handle_pool.hpp",
+ ],
+ deps = [
+ ":index_pool",
+ ],
+ visibility = ["//visibility:public"],
+)
+
cc_test(
name = "index_pool_tests",
srcs = [
@@ -56,3 +67,15 @@ cc_test(
"//asl/testing",
],
)
+
+cc_test(
+ name = "sparse_handle_pool_tests",
+ srcs = [
+ "sparse_handle_pool_tests.cpp",
+ ],
+ deps = [
+ ":sparse_handle_pool",
+ "//asl/tests:utils",
+ "//asl/testing",
+ ],
+)
diff --git a/asl/handle_pool/sparse_handle_pool.hpp b/asl/handle_pool/sparse_handle_pool.hpp
new file mode 100644
index 0000000..3f4535d
--- /dev/null
+++ b/asl/handle_pool/sparse_handle_pool.hpp
@@ -0,0 +1,158 @@
+// Copyright 2025 Steven Le Rouzic
+//
+// SPDX-License-Identifier: BSD-3-Clause
+
+#pragma once
+
+#include "asl/handle_pool/index_pool.hpp"
+#include "asl/memory/allocator.hpp"
+#include "asl/containers/chunked_buffer.hpp"
+
+
+namespace asl
+{
+
+// @Todo If we want the allocator to be non-copyable, we could
+// introduce a reference allocator type that is copyable, and store
+// the "main" allocator in the pool.
+
+template<
+ is_object T,
+ int kIndexBits,
+ int kGenBits,
+ typename UserType = empty,
+ int kUserBits = 0,
+ isize_t kChunkSize = 32,
+ allocator Allocator = DefaultAllocator>
+requires copyable<Allocator>
+class SparseHandlePool
+{
+ using ThisIndexPool = IndexPool<kIndexBits, kGenBits, UserType, kUserBits, empty, Allocator>;
+
+ struct Slot
+ {
+ ThisIndexPool::handle h;
+ maybe_uninit<T> obj;
+
+ Slot() = default;
+ ASL_DELETE_COPY_MOVE(Slot);
+
+ ~Slot()
+ {
+ if (!h.is_null())
+ {
+ obj.destroy_unsafe();
+ }
+ }
+ };
+
+ using Buffer = chunked_buffer<Slot, kChunkSize, Allocator>;
+
+ ThisIndexPool m_index_pool{};
+ Buffer m_buffer{};
+
+ using config = ThisIndexPool::handle::config;
+
+ template<typename... Args>
+ void set_object(ThisIndexPool::handle h, Args&&... args)
+ {
+ const auto index = static_cast<isize_t>(h.index());
+ if (m_buffer.size() <= index)
+ {
+ m_buffer.resize(index + 1);
+ }
+ m_buffer[index].h = h;
+ m_buffer[index].obj.construct_unsafe(std::forward<Args>(args)...);
+ }
+
+public:
+ using handle = ThisIndexPool::handle;
+
+ SparseHandlePool() requires default_constructible<Allocator> = default;
+
+ explicit SparseHandlePool(const Allocator& allocator)
+ : m_index_pool(allocator)
+ , m_buffer(allocator)
+ {}
+
+ ASL_DELETE_COPY(SparseHandlePool);
+ ASL_DEFAULT_MOVE(SparseHandlePool);
+ ~SparseHandlePool() = default;
+
+ [[nodiscard]] bool is_full() const
+ {
+ return m_index_pool.is_full();
+ }
+
+ bool is_valid(handle h) const
+ {
+ return m_index_pool.is_valid(h);
+ }
+
+ template<typename... Args>
+ option<handle> acquire(config::UserType user, Args&&... args)
+ requires config::kHasUser && constructible_from<T, Args&&...>
+ {
+ if (is_full()) { return nullopt; }
+ const auto handle = m_index_pool.acquire_ensure(user);
+ set_object(handle, std::forward<Args>(args)...);
+ return handle;
+ }
+
+ template<typename... Args>
+ option<handle> acquire(Args&&... args)
+ requires (!config::kHasUser) && constructible_from<T, Args&&...>
+ {
+ if (is_full()) { return nullopt; }
+ const auto handle = m_index_pool.acquire_ensure();
+ set_object(handle, std::forward<Args>(args)...);
+ return handle;
+ }
+
+ template<typename... Args>
+ handle acquire_ensure(config::UserType user, Args&&... args)
+ requires config::kHasUser && constructible_from<T, Args&&...>
+ {
+ ASL_ASSERT_RELEASE(!is_full());
+ const auto handle = m_index_pool.acquire_ensure(user);
+ set_object(handle, std::forward<Args>(args)...);
+ return handle;
+ }
+
+ template<typename... Args>
+ handle acquire_ensure(Args&&... args)
+ requires (!config::kHasUser) && constructible_from<T, Args&&...>
+ {
+ ASL_ASSERT_RELEASE(!is_full());
+ const auto handle = m_index_pool.acquire_ensure();
+ set_object(handle, std::forward<Args>(args)...);
+ return handle;
+ }
+
+ void release(handle h)
+ {
+ if (!is_valid(h)) { return; }
+ auto& slot = m_buffer[static_cast<isize_t>(h.index())];
+ slot.h = {};
+ slot.obj.destroy_unsafe();
+ m_index_pool.release(h);
+ }
+
+ auto get(this auto&& self, handle h)
+ -> copy_const_t<un_ref_t<decltype(self)>, T>*
+ {
+ if (!self.is_valid(h)) { return nullptr; }
+ return &std::forward<decltype(self)>(self).m_buffer[static_cast<isize_t>(h.index())]
+ .obj.as_init_unsafe();
+ }
+
+ auto get_ensure(this auto&& self, handle h)
+ -> copy_cref_t<decltype(self), T>
+ {
+ ASL_ASSERT_RELEASE(self.is_valid(h));
+ return std::forward<decltype(self)>(self).m_buffer[static_cast<isize_t>(h.index())]
+ .obj.as_init_unsafe();
+ }
+};
+
+} // namespace asl
diff --git a/asl/handle_pool/sparse_handle_pool_tests.cpp b/asl/handle_pool/sparse_handle_pool_tests.cpp
new file mode 100644
index 0000000..e7fbae9
--- /dev/null
+++ b/asl/handle_pool/sparse_handle_pool_tests.cpp
@@ -0,0 +1,117 @@
+// Copyright 2025 Steven Le Rouzic
+//
+// SPDX-License-Identifier: BSD-3-Clause
+
+#include "asl/testing/testing.hpp"
+#include "asl/tests/types.hpp"
+#include "asl/handle_pool/sparse_handle_pool.hpp"
+
+ASL_TEST(acquire_release) // NOLINT
+{
+ asl::SparseHandlePool<int, 1, 1> pool;
+
+ ASL_TEST_EXPECT(!pool.is_full());
+
+ const auto a = pool.acquire_ensure(6);
+ const auto b = pool.acquire_ensure(7);
+
+ ASL_TEST_EXPECT(pool.is_valid(a));
+ ASL_TEST_EXPECT(pool.is_valid(b));
+ ASL_TEST_EXPECT(pool.is_full());
+
+ ASL_TEST_EXPECT(pool.get_ensure(a) == 6);
+ ASL_TEST_EXPECT(pool.get_ensure(b) == 7);
+
+ pool.release(a);
+ ASL_TEST_EXPECT(!pool.is_valid(a));
+ ASL_TEST_EXPECT(pool.is_valid(b));
+ ASL_TEST_EXPECT(!pool.is_full());
+ ASL_TEST_EXPECT(pool.get(a) == nullptr);
+ ASL_TEST_EXPECT(*pool.get(b) == 7);
+
+ const auto c = pool.acquire_ensure(8);
+
+ ASL_TEST_EXPECT(!pool.is_valid(a));
+ ASL_TEST_EXPECT(pool.is_valid(b));
+ ASL_TEST_EXPECT(pool.is_valid(c));
+ ASL_TEST_EXPECT(pool.is_full());
+
+ ASL_TEST_EXPECT(*pool.get(b) == 7);
+ ASL_TEST_EXPECT(*pool.get(c) == 8);
+ ASL_TEST_EXPECT(pool.get(a) == nullptr);
+}
+
+ASL_TEST(element_destructor)
+{
+ asl::SparseHandlePool<DestructorObserver, 8, 8> pool;
+ bool d[3]{};
+
+ const auto d0 = pool.acquire_ensure(&d[0]);
+ const auto d1 = pool.acquire_ensure(&d[1]);
+ const auto d2 = pool.acquire_ensure(&d[2]);
+
+ ASL_TEST_EXPECT(!d[0]);
+ ASL_TEST_EXPECT(!d[1]);
+ ASL_TEST_EXPECT(!d[2]);
+
+ pool.release(d1);
+ ASL_TEST_EXPECT(!d[0]);
+ ASL_TEST_EXPECT(d[1]);
+ ASL_TEST_EXPECT(!d[2]);
+
+ pool.release(d0);
+ ASL_TEST_EXPECT(d[0]);
+ ASL_TEST_EXPECT(d[1]);
+ ASL_TEST_EXPECT(!d[2]);
+
+ pool.release(d2);
+ ASL_TEST_EXPECT(d[0]);
+ ASL_TEST_EXPECT(d[1]);
+ ASL_TEST_EXPECT(d[2]);
+}
+
+ASL_TEST(destructor)
+{
+ bool d[3]{};
+
+ {
+ asl::SparseHandlePool<DestructorObserver, 8, 8> pool;
+
+ pool.acquire_ensure(&d[0]);
+ const auto d1 = pool.acquire_ensure(&d[1]);
+ pool.acquire_ensure(&d[2]);
+
+ ASL_TEST_EXPECT(!d[0]);
+ ASL_TEST_EXPECT(!d[1]);
+ ASL_TEST_EXPECT(!d[2]);
+
+ pool.release(d1);
+ ASL_TEST_EXPECT(!d[0]);
+ ASL_TEST_EXPECT(d[1]);
+ ASL_TEST_EXPECT(!d[2]);
+ }
+
+ ASL_TEST_EXPECT(d[0]);
+ ASL_TEST_EXPECT(d[1]);
+ ASL_TEST_EXPECT(d[2]);
+}
+
+enum Flags : uint8_t
+{
+ kFlag1 = 1,
+ kFlag2 = 2,
+};
+
+ASL_TEST(user_type)
+{
+ asl::SparseHandlePool<int, 8, 8, Flags> pool;
+
+ auto a = pool.acquire_ensure(kFlag2, 22);
+ auto b = pool.acquire_ensure(kFlag1, 11);
+
+ ASL_TEST_EXPECT(pool.get_ensure(a) == 22);
+ ASL_TEST_EXPECT(pool.get_ensure(b) == 11);
+
+ ASL_TEST_EXPECT(a.user() == kFlag2);
+ ASL_TEST_EXPECT(b.user() == kFlag1);
+}