Files
Commonwealth-Online-Public/plugin/lib/commonlibf4/include/RE/B/BSTHashMap.h
T
2026-05-30 18:33:24 +12:00

781 lines
22 KiB
C++

#pragma once
#include "RE/B/BSCRC32.h"
#include "RE/B/BSTTuple.h"
#include "RE/M/MemoryManager.h"
namespace RE
{
namespace detail
{
static constexpr std::uint8_t BSTScatterTableSentinel[] = { 0xDEu, 0xADu, 0xBEu, 0xEFu };
}
// scatter table with chaining
template <
class Hash,
class KeyEqual,
class Traits,
template <std::size_t, std::size_t> class Allocator>
class BSTScatterTable
{
public:
using traits_type = Traits;
using key_type = typename Traits::key_type;
using mapped_type = typename Traits::mapped_type;
using value_type = typename Traits::value_type;
using size_type = std::uint32_t;
using difference_type = std::int32_t;
using hasher = Hash;
using key_equal = KeyEqual;
using reference = value_type&;
using const_reference = const value_type&;
using pointer = value_type*;
using const_pointer = const value_type*;
static_assert(std::is_invocable_r_v<size_type, const hasher&, const key_type&>);
static_assert(std::is_invocable_r_v<bool, const key_equal&, const key_type&, const key_type&>);
private:
struct entry_type
{
entry_type() = default;
entry_type(const entry_type&) = delete;
entry_type(entry_type&& a_rhs) //
noexcept(std::is_nothrow_move_constructible_v<value_type> &&
std::is_nothrow_destructible_v<value_type>)
{
if (a_rhs.has_value()) {
const auto rnext = a_rhs.next;
emplace(std::move(a_rhs).steal(), rnext);
}
}
~entry_type() noexcept { destroy(); };
entry_type& operator=(const entry_type&) = delete;
entry_type& operator=(entry_type&& a_rhs) //
noexcept(std::is_nothrow_move_constructible_v<value_type> &&
std::is_nothrow_destructible_v<value_type>)
{
if (this != std::addressof(a_rhs)) {
destroy();
if (a_rhs.has_value()) {
const auto rnext = a_rhs.next;
emplace(std::move(a_rhs).steal(), rnext);
}
}
return *this;
}
[[nodiscard]] bool has_value() const noexcept { return next != nullptr; }
void destroy() //
noexcept(std::is_nothrow_destructible_v<value_type>)
{
if (has_value()) {
std::destroy_at(std::addressof(value));
next = nullptr;
}
assert(!has_value());
}
template <class Arg>
void emplace(Arg&& a_value, const entry_type* a_next) //
noexcept(std::is_nothrow_constructible_v<value_type, Arg>)
{
static_assert(std::same_as<std::decay_t<Arg>, value_type>);
destroy();
std::construct_at(std::addressof(value), std::forward<Arg>(a_value));
next = const_cast<entry_type*>(a_next);
assert(has_value());
}
[[nodiscard]] value_type steal() && //
noexcept(std::is_nothrow_move_constructible_v<value_type> &&
std::is_nothrow_destructible_v<value_type>)
{
assert(has_value());
value_type val = std::move(value);
destroy();
assert(!has_value());
return val;
}
union
{
value_type value;
std::byte buffer[sizeof(value_type)]{ static_cast<std::byte>(0) };
};
entry_type* next{ nullptr };
};
template <class U>
class iterator_base
{
public:
using difference_type = std::ptrdiff_t;
using value_type = std::remove_const_t<U>;
using pointer = value_type*;
using reference = value_type&;
using iterator_category = std::forward_iterator_tag;
iterator_base() = default;
template <class V>
iterator_base(const iterator_base<V>& a_rhs) noexcept //
requires(std::convertible_to<typename iterator_base<V>::reference, reference>)
:
_first(a_rhs._first),
_last(a_rhs._last)
{}
~iterator_base() = default;
template <class V>
iterator_base& operator=(const iterator_base<V>& a_rhs) noexcept //
requires(std::convertible_to<typename iterator_base<V>::reference, reference>)
{
assert(_last == a_rhs._last);
_first = a_rhs._first;
_last = a_rhs._last;
return *this;
}
[[nodiscard]] reference operator*() const noexcept
{
assert(iterable());
assert(_first->has_value());
return _first->value;
}
[[nodiscard]] pointer operator->() const noexcept
{
return std::pointer_traits<pointer>::pointer_to(operator*());
}
template <class V>
[[nodiscard]] bool operator==(const iterator_base<V>& a_rhs) const noexcept
{
assert(_last == a_rhs._last);
return _first == a_rhs._first;
}
template <class V>
[[nodiscard]] bool operator!=(const iterator_base<V>& a_rhs) const noexcept
{
return !operator==(a_rhs);
}
iterator_base& operator++() noexcept
{
seek();
return *this;
}
iterator_base operator++(int) noexcept
{
iterator_base tmp{ *this };
operator++();
return tmp;
}
protected:
friend class BSTScatterTable;
iterator_base(entry_type* a_first, entry_type* a_last) noexcept :
_first(a_first),
_last(a_last)
{
assert(!!_first == !!_last); // both or neither have values
assert(_first <= _last);
if (iterable() && !_first->has_value()) {
seek();
}
}
[[nodiscard]] entry_type* get_entry() const noexcept { return _first; }
private:
template <class>
friend class iterator_base;
[[nodiscard]] bool iterable() const noexcept { return _first && _last && _first != _last; }
void seek() noexcept
{
assert(iterable());
do {
++_first;
} while (_first != _last && !_first->has_value());
}
entry_type* _first{ nullptr };
entry_type* _last{ nullptr };
};
public:
using allocator_type = Allocator<sizeof(entry_type), alignof(entry_type)>;
using iterator = iterator_base<value_type>;
using const_iterator = iterator_base<const value_type>;
BSTScatterTable() = default;
BSTScatterTable(const BSTScatterTable& a_rhs) { insert(a_rhs.begin(), a_rhs.end()); }
BSTScatterTable(BSTScatterTable&& a_rhs) noexcept //
requires(std::same_as<typename allocator_type::propagate_on_container_move_assignment, std::true_type>)
:
_capacity(std::exchange(a_rhs._capacity, 0)),
_free(std::exchange(a_rhs._free, 0)),
_good(std::exchange(a_rhs._good, 0)),
_sentinel(a_rhs._sentinel),
_allocator(std::move(a_rhs._allocator))
{
assert(a_rhs.empty());
}
~BSTScatterTable() { free_resources(); }
BSTScatterTable& operator=(const BSTScatterTable& a_rhs)
{
if (this != std::addressof(a_rhs)) {
clear();
insert(a_rhs.begin(), a_rhs.end());
}
return *this;
}
BSTScatterTable& operator=(BSTScatterTable&& a_rhs) //
requires(std::same_as<typename allocator_type::propagate_on_container_move_assignment, std::true_type>)
{
if (this != std::addressof(a_rhs)) {
free_resources();
_capacity = std::exchange(a_rhs._capacity, 0);
_free = std::exchange(a_rhs._free, 0);
_good = std::exchange(a_rhs._good, 0);
_sentinel = a_rhs._sentinel;
_allocator = std::move(a_rhs._allocator);
assert(a_rhs.empty());
}
return *this;
}
[[nodiscard]] iterator begin() noexcept { return make_iterator<iterator>(get_entries()); }
[[nodiscard]] const_iterator begin() const noexcept { return make_iterator<const_iterator>(get_entries()); }
[[nodiscard]] const_iterator cbegin() const noexcept { return make_iterator<const_iterator>(get_entries()); }
[[nodiscard]] iterator end() noexcept { return make_iterator<iterator>(); }
[[nodiscard]] const_iterator end() const noexcept { return make_iterator<const_iterator>(); }
[[nodiscard]] const_iterator cend() const noexcept { return make_iterator<const_iterator>(); }
[[nodiscard]] bool empty() const noexcept { return size() == 0; }
[[nodiscard]] size_type size() const noexcept { return _capacity - _free; }
void clear()
{
if (size() > 0) {
const auto entries = get_entries();
assert(entries != nullptr);
for (size_type i = 0; i < _capacity; ++i) {
entries[i].destroy();
}
_free = _capacity;
_good = 0;
}
assert(empty());
}
std::pair<iterator, bool> insert(const value_type& a_value) { return do_insert(a_value); }
std::pair<iterator, bool> insert(value_type&& a_value) { return do_insert(std::move(a_value)); }
template <std::input_iterator InputIt>
void insert(InputIt a_first, InputIt a_last) //
requires(std::convertible_to<std::iter_reference_t<InputIt>, const_reference>)
{
reserve(size() + static_cast<size_type>(std::distance(a_first, a_last)));
for (; a_first != a_last; ++a_first) {
insert(*std::move(a_first));
}
}
template <class... Args>
std::pair<iterator, bool> emplace(Args&&... a_args) //
requires(std::constructible_from<value_type, Args...>)
{
return insert(value_type(std::forward<Args>(a_args)...));
}
iterator erase(const_iterator a_pos) { return do_erase(a_pos); }
iterator erase(iterator a_pos) { return do_erase(a_pos); }
size_type erase(const key_type& a_key)
{
const auto pos = find(a_key);
const auto result = pos != end() ? erase(pos) : pos;
return result != end() ? 1 : 0;
}
[[nodiscard]] iterator find(const key_type& a_key) { return do_find<iterator>(a_key); }
[[nodiscard]] const_iterator find(const key_type& a_key) const { return do_find<const_iterator>(a_key); }
[[nodiscard]] bool contains(const key_type& a_key) const { return find(a_key) != end(); }
void reserve(size_type a_count)
{
if (a_count <= _capacity) {
return;
}
const auto oldCap = _capacity;
const auto oldEntries = get_entries();
const auto [newCap, newEntries] = [&]() {
constexpr std::uint64_t min = allocator_type::min_size();
static_assert(min > 0 && std::has_single_bit(min));
const auto cap = std::max(std::bit_ceil<std::uint64_t>(a_count), min);
assert(cap >= min);
if (cap > 1u << 31) {
REX::FAIL("a buffer grew too large"sv);
}
const auto entries = allocate(static_cast<size_type>(cap));
if (!entries) {
REX::FAIL("failed to handle an allocation"sv);
}
return std::make_pair(static_cast<size_type>(cap), entries);
}();
const auto setCap = [&](size_type a_newCap) {
_capacity = a_newCap;
_free = _capacity;
_good = 0;
};
if (newEntries == oldEntries) {
std::uninitialized_default_construct_n(oldEntries + oldCap, newCap - oldCap);
std::vector<value_type> todo;
todo.reserve(size());
for (size_type i = 0; i < oldCap; ++i) {
auto& entry = oldEntries[i];
if (entry.has_value()) {
todo.emplace_back(std::move(entry).steal());
}
}
setCap(newCap);
insert(
std::make_move_iterator(todo.begin()),
std::make_move_iterator(todo.end()));
} else {
// in with the new
std::uninitialized_default_construct_n(newEntries, newCap);
setCap(newCap);
set_entries(newEntries);
if (oldEntries) { // out with the old
for (size_type i = 0; i < oldCap; ++i) {
auto& entry = oldEntries[i];
if (entry.has_value()) {
insert(std::move(entry).steal());
}
}
std::destroy_n(oldEntries, oldCap);
deallocate(oldEntries);
}
}
}
private:
[[nodiscard]] static const key_type& unwrap_key(const value_type& a_value) noexcept
{
return traits_type::unwrap_key(a_value);
}
[[nodiscard]] entry_type* allocate(size_type a_count)
{
return static_cast<entry_type*>(_allocator.allocate_bytes(sizeof(entry_type) * a_count));
}
void deallocate(entry_type* a_entry) { _allocator.deallocate_bytes(a_entry); }
[[nodiscard]] iterator do_erase(const_iterator a_pos)
{
assert(a_pos != end());
const auto entry = a_pos.get_entry();
assert(entry != nullptr);
assert(entry->has_value());
if (entry->next == _sentinel) { // end of chain
if (auto prev = &get_entry_for(unwrap_key(entry->value)); prev != entry) {
while (prev->next != entry) {
prev = prev->next;
}
prev->next = const_cast<entry_type*>(_sentinel); // detach from chain
}
entry->destroy();
} else { // move next into current
*entry = std::move(*entry->next);
}
++_free;
return make_iterator<iterator>(entry + 1);
}
template <class Iter>
[[nodiscard]] Iter do_find(const key_type& a_key) const //
noexcept(noexcept(hash_function(a_key)) && noexcept(key_eq(a_key, a_key)))
{
if (empty()) {
return make_iterator<Iter>();
}
auto entry = &get_entry_for(a_key);
if (entry->has_value()) {
do { // follow chain
if (key_eq(unwrap_key(entry->value), a_key)) {
return make_iterator<Iter>(entry);
} else {
entry = entry->next;
}
} while (entry != _sentinel);
}
return make_iterator<Iter>();
}
template <class P>
[[nodiscard]] std::pair<iterator, bool> do_insert(P&& a_value) //
requires(std::same_as<std::decay_t<P>, value_type>)
{
if (const auto it = find(unwrap_key(a_value)); it != end()) { // already exists
return std::make_pair(it, false);
}
if (_free == 0) { // no free entries
reserve(_capacity + 1);
assert(_free > 0);
}
const REX::TScopeExit decrement{ [&]() noexcept { --_free; } };
const auto entry = &get_entry_for(unwrap_key(a_value));
if (entry->has_value()) { // slot is taken, resolve conflict
const auto free = &get_free_entry();
const auto wouldve = &get_entry_for(unwrap_key(entry->value));
if (wouldve == entry) { // hash collision
free->emplace(std::forward<P>(a_value), std::exchange(entry->next, free));
return std::make_pair(make_iterator<iterator>(free), true);
} else { // how did we get here?
auto prev = wouldve;
while (prev->next != entry) {
prev = prev->next;
}
// evict current value and detach from chain
*free = std::move(*entry);
prev->next = free;
entry->emplace(std::forward<P>(a_value), _sentinel);
return std::make_pair(make_iterator<iterator>(entry), true);
}
} else { // its free realestate
entry->emplace(std::forward<P>(a_value), _sentinel);
return std::make_pair(make_iterator<iterator>(entry), true);
}
}
void free_resources()
{
if (_capacity > 0) {
assert(get_entries() != nullptr);
std::destroy_n(get_entries(), _capacity);
deallocate(get_entries());
set_entries(nullptr);
_capacity = 0;
_free = 0;
_good = 0;
}
assert(get_entries() == nullptr);
assert(_capacity == 0);
assert(_free == 0);
}
[[nodiscard]] entry_type& get_entry_for(const key_type& a_key) const //
noexcept(noexcept(hash_function(a_key)))
{
assert(get_entries() != nullptr);
assert(std::has_single_bit(_capacity));
const auto hash = hash_function(a_key);
const auto idx = hash & (_capacity - 1); // quick modulo
return get_entries()[idx];
}
[[nodiscard]] entry_type* get_entries() const noexcept { return static_cast<entry_type*>(_allocator.get_entries()); }
[[nodiscard]] entry_type& get_free_entry() noexcept
{
assert(_free > 0);
assert(get_entries() != nullptr);
assert(std::has_single_bit(_capacity));
assert([&]() noexcept {
const auto begin = get_entries();
const auto end = get_entries() + _capacity;
return std::find_if(
begin,
end,
[](const auto& a_entry) noexcept {
return !a_entry.has_value();
}) != end;
}());
const auto entries = get_entries();
while (entries[_good].has_value()) {
_good = (_good + 1) & (_capacity - 1); // wrap around w/ quick modulo
}
return entries[_good];
}
[[nodiscard]] size_type hash_function(const key_type& a_key) const //
noexcept(std::is_nothrow_constructible_v<hasher> &&
std::is_nothrow_invocable_v<const hasher&, const key_type&>)
{
return static_cast<size_type>(hasher()(a_key));
}
[[nodiscard]] bool key_eq(const key_type& a_lhs, const key_type& a_rhs) const //
noexcept(std::is_nothrow_constructible_v<key_equal> &&
std::is_nothrow_invocable_v<const key_equal&, const key_type&, const key_type&>)
{
return static_cast<bool>(key_equal()(a_lhs, a_rhs));
}
template <class Iter>
[[nodiscard]] Iter make_iterator() const noexcept
{
return Iter(get_entries() + _capacity, get_entries() + _capacity);
}
template <class Iter>
[[nodiscard]] Iter make_iterator(entry_type* a_first) const noexcept
{
return Iter(a_first, get_entries() + _capacity);
}
void set_entries(entry_type* a_entries) noexcept { _allocator.set_entries(a_entries); }
// members
std::uint64_t _pad00{ 0 }; // 00
std::uint32_t _pad08{ 0 }; // 08
size_type _capacity{ 0 }; // 0C - total # of slots, always a power of 2
size_type _free{ 0 }; // 10 - # of free slots
size_type _good{ 0 }; // 14 - last free index
const entry_type* _sentinel{ reinterpret_cast<const entry_type*>(detail::BSTScatterTableSentinel) }; // 18 - signals end of chain
allocator_type _allocator; // 20
};
template <class Key, class T>
class BSTScatterTableTraits
{
public:
using key_type = Key;
using mapped_type = T;
using value_type = RE::BSTTuple<const key_type, mapped_type>;
[[nodiscard]] static const key_type& unwrap_key(const value_type& a_value) noexcept { return a_value.first; }
};
template <class Key>
class BSTSetTraits
{
public:
using key_type = Key;
using mapped_type = void;
using value_type = key_type;
[[nodiscard]] static const key_type& unwrap_key(const value_type& a_value) noexcept { return a_value; }
};
template <std::size_t S, std::size_t A>
class BSTScatterTableHeapAllocator
{
public:
using size_type = std::uint32_t;
using propagate_on_container_move_assignment = std::true_type;
BSTScatterTableHeapAllocator() = default;
BSTScatterTableHeapAllocator(const BSTScatterTableHeapAllocator&) = delete;
BSTScatterTableHeapAllocator(BSTScatterTableHeapAllocator&& a_rhs) noexcept :
_entries(std::exchange(a_rhs._entries, nullptr))
{}
~BSTScatterTableHeapAllocator() = default;
BSTScatterTableHeapAllocator& operator=(const BSTScatterTableHeapAllocator&) = delete;
BSTScatterTableHeapAllocator& operator=(BSTScatterTableHeapAllocator&& a_rhs) noexcept
{
if (this != std::addressof(a_rhs)) {
assert(_entries == nullptr);
_entries = std::exchange(a_rhs._entries, nullptr);
}
return *this;
}
[[nodiscard]] static constexpr size_type min_size() noexcept { return 1u << 3; }
[[nodiscard]] void* allocate_bytes(std::size_t a_bytes)
{
assert(a_bytes % S == 0);
return malloc(a_bytes);
}
void deallocate_bytes(void* a_ptr) { free(a_ptr); }
[[nodiscard]] void* get_entries() const noexcept { return _entries; }
void set_entries(void* a_entries) noexcept { _entries = static_cast<std::byte*>(a_entries); }
private:
// members
std::uint64_t _pad00{ 0 }; // 00 (20)
std::byte* _entries{ nullptr }; // 08 (28)
};
template <std::uint32_t N>
class BSTStaticHashMapBase
{
public:
static_assert(N > 0 && std::has_single_bit(N));
template <std::size_t S, std::size_t A>
class Allocator
{
public:
using size_type = std::uint32_t;
using propagate_on_container_move_assignment = std::false_type;
Allocator() = default;
Allocator(const Allocator&) = delete;
Allocator(Allocator&&) = delete;
~Allocator() = default;
Allocator& operator=(const Allocator&) = delete;
Allocator& operator=(Allocator&&) = delete;
[[nodiscard]] static constexpr size_type min_size() noexcept { return N; }
[[nodiscard]] void* allocate_bytes(std::size_t a_bytes)
{
assert(a_bytes % S == 0);
return a_bytes <= N * S ? _buffer : nullptr;
}
void deallocate_bytes([[maybe_unused]] void* a_ptr) { assert(a_ptr == _buffer); }
[[nodiscard]] void* get_entries() const noexcept { return _entries; }
void set_entries(void* a_entries) noexcept
{
assert(a_entries == _buffer || a_entries == nullptr);
_entries = static_cast<std::byte*>(a_entries);
}
private:
alignas(A) std::byte _buffer[N * S]{ static_cast<std::byte>(0) }; // 00 (20)
std::byte* _entries{ nullptr }; // ??
};
};
template <std::size_t S, std::size_t A>
class BSTScatterTableScrapAllocator
{
public:
using size_type = std::uint32_t;
using propagate_on_container_move_assignment = std::false_type;
BSTScatterTableScrapAllocator() = default;
BSTScatterTableScrapAllocator(const BSTScatterTableScrapAllocator&) = delete;
BSTScatterTableScrapAllocator(BSTScatterTableScrapAllocator&&) = delete;
~BSTScatterTableScrapAllocator() = default;
BSTScatterTableScrapAllocator& operator=(const BSTScatterTableScrapAllocator&) = delete;
BSTScatterTableScrapAllocator& operator=(BSTScatterTableScrapAllocator&&) = delete;
[[nodiscard]] static constexpr size_type min_size() noexcept { return 1u << 3; }
[[nodiscard]] void* allocate_bytes(std::size_t a_bytes)
{
assert(_allocator != nullptr);
assert(a_bytes % S == 0);
return _allocator->Allocate(a_bytes, 0x10);
}
void deallocate_bytes(void* a_ptr)
{
assert(_allocator != nullptr);
_allocator->Deallocate(a_ptr);
}
[[nodiscard]] void* get_entries() const noexcept { return _entries; }
void set_entries(void* a_entries) noexcept { _entries = static_cast<std::byte*>(a_entries); }
private:
// members
ScrapHeap* _allocator{ MemoryManager::GetSingleton().GetThreadScrapHeap() }; // 00 (20)
std::byte* _entries{ nullptr }; // 08 (28)
};
template <
class Key,
class T,
class Hash = BSCRC32<Key>,
class KeyEq = std::equal_to<Key>>
using BSTHashMap =
BSTScatterTable<
Hash,
KeyEq,
BSTScatterTableTraits<Key, T>,
BSTScatterTableHeapAllocator>;
template <
class Key,
class Hash = BSCRC32<Key>,
class KeyEq = std::equal_to<Key>>
using BSTSet =
BSTScatterTable<
Hash,
KeyEq,
BSTSetTraits<Key>,
BSTScatterTableHeapAllocator>;
template <
class Key,
class T,
std::uint32_t N,
class Hash = BSCRC32<Key>,
class KeyEq = std::equal_to<Key>>
using BSTStaticHashMap =
BSTScatterTable<
Hash,
KeyEq,
BSTScatterTableTraits<Key, T>,
BSTStaticHashMapBase<N>::template Allocator>;
template <
class Key,
class T,
class Hash = BSCRC32<Key>,
class KeyEq = std::equal_to<Key>>
using BSTScrapHashMap =
BSTScatterTable<
Hash,
KeyEq,
BSTScatterTableTraits<Key, T>,
BSTScatterTableScrapAllocator>;
}