Add SparseHandlePool

This commit is contained in:
2025-06-26 18:36:24 +02:00
parent a78e24e9eb
commit 195f20ff17
3 changed files with 298 additions and 0 deletions

View File

@ -32,6 +32,17 @@ cc_library(
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
) )
cc_library(
name = "sparse_handle_pool",
hdrs = [
"sparse_handle_pool.hpp",
],
deps = [
":index_pool",
],
visibility = ["//visibility:public"],
)
cc_test( cc_test(
name = "index_pool_tests", name = "index_pool_tests",
srcs = [ srcs = [
@ -56,3 +67,15 @@ cc_test(
"//asl/testing", "//asl/testing",
], ],
) )
cc_test(
name = "sparse_handle_pool_tests",
srcs = [
"sparse_handle_pool_tests.cpp",
],
deps = [
":sparse_handle_pool",
"//asl/tests:utils",
"//asl/testing",
],
)

View File

@ -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

View File

@ -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);
}