Files
UniversalContainer/include/universal_container/flat_hash_index.hpp
T
2026-08-13 23:42:26 +03:00

300 lines
11 KiB
C++

#pragma once
#include <cstddef>
#include <cstdint>
#include <functional>
#include <limits>
#include <optional>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <vector>
namespace uc::detail {
// Flat open-addressed value -> duplicate-chain metadata. The actual duplicate
// links live in AdaptiveSequence's dense ID table, so one bucket is paid per
// distinct value rather than one node allocation per element.
template <class Key, class Hash = std::hash<Key>, class Equal = std::equal_to<Key>>
class FlatDuplicateIndex {
public:
using id_type = std::uint32_t;
static constexpr id_type invalid_id = std::numeric_limits<id_type>::max();
struct Entry {
id_type head = invalid_id;
std::uint32_t count = 0;
};
FlatDuplicateIndex() { rehash(16); }
[[nodiscard]] const Entry* find(const Key& key) const {
const auto position = find_existing(key);
return position == npos ? nullptr : &buckets_[position].entry;
}
[[nodiscard]] Entry* find(const Key& key) {
const auto position = find_existing(key);
return position == npos ? nullptr : &buckets_[position].entry;
}
Entry& ensure(const Key& key) {
// A moved-from std::vector is allowed to be empty while the scalar
// counters retain their old values. Rebuild lazily so a moved-from
// index remains reusable and mask() is never evaluated for zero
// buckets.
if (buckets_.empty()) {
rehash(16);
}
auto [position, found] = find_insert_position(key);
if (position == npos) {
// Normal growth is performed by reserve_for_elements() at a
// logical container-capacity change. Reaching a completely full
// table means that invariant was unavailable (most notably after
// reusing a moved-from standalone index), so grow only as a
// recovery path rather than as a load-factor policy.
if (buckets_.size() > std::numeric_limits<std::size_t>::max() / 2) {
throw std::length_error("flat hash index capacity overflow");
}
rehash(buckets_.size() * 2);
const auto retry = find_insert_position(key);
position = retry.first;
found = retry.second;
if (position == npos) {
throw std::length_error("flat hash index is full");
}
}
auto& bucket = buckets_[position];
if (!found) {
bucket.key.emplace(key);
if (bucket.state == State::tombstone) {
--tombstones_;
}
bucket.entry = {};
bucket.state = State::occupied;
++size_;
}
return bucket.entry;
}
bool erase_key(const Key& key) {
const auto position = find_existing(key);
if (position == npos) {
return false;
}
if constexpr (std::is_nothrow_invocable_v<const Hash&, const Key&>
&& std::is_nothrow_move_assignable_v<Bucket>) {
erase_and_compact(position);
} else {
// Computing hashes or moving a key can throw. Rebuild a complete
// replacement without the erased key, then commit with swap, so a
// failed erase never opens a hole in the live probe chain.
erase_transactional(position);
}
--size_;
return true;
}
void clear() {
// Allocate before committing so a failed reset leaves the live table
// and its duplicate metadata untouched.
std::vector<Bucket> replacement(16);
buckets_.swap(replacement);
size_ = 0;
tombstones_ = 0;
}
// Used by the owning container after moving the live table elsewhere.
// Keeping zero buckets avoids an allocation in its noexcept move path;
// ensure()/reserve_for_elements() restore the minimum table lazily.
void reset_after_move() noexcept {
buckets_.clear();
size_ = 0;
tombstones_ = 0;
}
[[nodiscard]] std::size_t distinct_values() const noexcept { return size_; }
[[nodiscard]] std::size_t bucket_count() const noexcept { return buckets_.size(); }
[[nodiscard]] static std::size_t buckets_for_elements(
std::size_t element_capacity) {
// At most one distinct key per element. Keeping the worst-case load
// below 70% guarantees that ensure() cannot trigger an independent
// rehash between container capacity changes.
const auto required = element_capacity > (std::numeric_limits<std::size_t>::max() - 6) / 10
? std::numeric_limits<std::size_t>::max()
: (element_capacity * 10 + 6) / 7;
std::size_t buckets = 16;
while (buckets < required) {
if (buckets > std::numeric_limits<std::size_t>::max() / 2) {
throw std::length_error("flat hash index capacity overflow");
}
buckets *= 2;
}
return buckets;
}
void reserve_for_elements(std::size_t element_capacity) {
const auto requested = buckets_for_elements(element_capacity);
if (requested != buckets_.size() || tombstones_ != 0) {
rehash(requested);
}
}
[[nodiscard]] std::size_t allocated_bytes() const noexcept {
return buckets_.capacity() * sizeof(Bucket);
}
private:
enum class State : std::uint8_t { empty, occupied, tombstone };
struct Bucket {
std::optional<Key> key;
Entry entry;
State state = State::empty;
};
static constexpr std::size_t npos = std::numeric_limits<std::size_t>::max();
[[nodiscard]] std::size_t mask() const noexcept { return buckets_.size() - 1; }
[[nodiscard]] std::size_t find_existing(const Key& key) const {
if (buckets_.empty()) {
return npos;
}
auto position = hasher_(key) & mask();
for (std::size_t probe = 0; probe < buckets_.size(); ++probe) {
const auto& bucket = buckets_[position];
if (bucket.state == State::empty) {
return npos;
}
if (bucket.state == State::occupied && equal_(*bucket.key, key)) {
return position;
}
position = (position + 1) & mask();
}
return npos;
}
[[nodiscard]] std::pair<std::size_t, bool>
find_insert_position(const Key& key) const {
return find_insert_position_in(buckets_, key);
}
[[nodiscard]] std::pair<std::size_t, bool>
find_insert_position_in(const std::vector<Bucket>& buckets,
const Key& key) const {
if (buckets.empty()) {
return {npos, false};
}
const auto table_mask = buckets.size() - 1;
auto position = hasher_(key) & table_mask;
auto first_tombstone = npos;
for (std::size_t probe = 0; probe < buckets.size(); ++probe) {
const auto& bucket = buckets[position];
if (bucket.state == State::empty) {
return {first_tombstone == npos ? position : first_tombstone, false};
}
if (bucket.state == State::tombstone) {
if (first_tombstone == npos) {
first_tombstone = position;
}
} else if (equal_(*bucket.key, key)) {
return {position, true};
}
position = (position + 1) & table_mask;
}
if (first_tombstone != npos) {
return {first_tombstone, false};
}
return {npos, false};
}
void erase_and_compact(std::size_t position) {
auto hole = position;
buckets_[hole] = Bucket{};
auto current = (hole + 1) & mask();
while (buckets_[current].state != State::empty) {
auto& bucket = buckets_[current];
if (bucket.state == State::occupied) {
const auto home = hasher_(*bucket.key) & mask();
const auto current_distance = (current - home) & mask();
const auto hole_distance = (hole - home) & mask();
if (hole_distance < current_distance) {
buckets_[hole] = std::move(bucket);
bucket = Bucket{};
hole = current;
}
}
current = (current + 1) & mask();
}
}
void erase_transactional(std::size_t erased_position) {
std::vector<Bucket> replacement(buckets_.size());
for (std::size_t source = 0; source < buckets_.size(); ++source) {
const auto& bucket = buckets_[source];
if (source == erased_position || bucket.state != State::occupied) {
continue;
}
const auto [position, found] =
find_insert_position_in(replacement, *bucket.key);
if (position == npos || found) {
throw std::logic_error("flat hash index rebuild invariant violated");
}
auto& target = replacement[position];
target.key.emplace(*bucket.key);
target.entry = bucket.entry;
target.state = State::occupied;
}
buckets_.swap(replacement);
tombstones_ = 0;
}
void rehash(std::size_t requested_capacity) {
std::size_t capacity = 16;
while (capacity < requested_capacity) {
if (capacity > std::numeric_limits<std::size_t>::max() / 2) {
throw std::length_error("flat hash index capacity overflow");
}
capacity *= 2;
}
// Build the complete replacement before touching the live table. An
// allocation, hash/equality call, or copy of a copyable Key may throw;
// in all of those cases the original buckets and counters remain
// unchanged. vector::swap is the no-throw commit for std::allocator.
std::vector<Bucket> replacement(capacity);
std::size_t replacement_size = 0;
for (const auto& bucket : buckets_) {
if (bucket.state != State::occupied) {
continue;
}
const auto [position, found] =
find_insert_position_in(replacement, *bucket.key);
if (position == npos) {
throw std::length_error("flat hash index is full");
}
if (found) {
throw std::logic_error("flat hash index contains duplicate keys");
}
auto& target = replacement[position];
target.key.emplace(*bucket.key);
target.entry = bucket.entry;
target.state = State::occupied;
++replacement_size;
}
buckets_.swap(replacement);
size_ = replacement_size;
tombstones_ = 0;
}
Hash hasher_{};
Equal equal_{};
std::vector<Bucket> buckets_;
std::size_t size_ = 0;
std::size_t tombstones_ = 0;
};
} // namespace uc::detail