roo_collections
API Documentation for roo_collections
Loading...
Searching...
No Matches
flat_small_hashtable.h
Go to the documentation of this file.
1#pragma once
2
3/// @file
4/// @brief Flat, memory-conscious hash table implementation.
5/// @ingroup roo_collections
6
7#include <assert.h>
8#include <inttypes.h>
9
10#include <functional>
11#include <initializer_list>
12#include <memory>
13
14#include "roo_backport.h"
15#include "roo_backport/string_view.h"
18
19#ifdef ARDUINO
20#include <WString.h>
21
22// Adding coparators that migt be missing on some platforms (notably RPI 2040).
23
24inline bool operator==(const ::String& a, roo::string_view b) {
25 return roo::string_view(a.c_str(), a.length()) == b;
26}
27
28inline bool operator==(roo::string_view a, const String& b) { return (b == a); }
29
30#endif
31
32namespace roo_collections {
33
34// Slightly higher than conventional 0.7, mostly so that the default-capacity
35// small hashtable (with ht_len 11) can hold 8 elements.
36static constexpr float kMaxFillRatio = 0.73;
37
38// Sequence of the largest primes of the format 4n+3, less than 2^k,
39// for k = 2 ... 16. When used as hash map capacities, they are known to
40// enable quadratic residue search to visit the entire array. Additionally,
41// we keep the value '1' at the beginning, as the sentinel that indicates
42// an empty hashtable, with no dynamically allocated buffer.
43static constexpr uint16_t kRadkePrimes[] = {
44 0x1, 0x3, 0x7, 0xb, 0x1f, 0x3b, 0x7f, 0xfb,
45 0x1f7, 0x3fb, 0x7f7, 0xffb, 0x1fff, 0x3feb, 0x7fcf, 0xffef};
46
47// These are precalculated (2^48 - 1) / the corresponding radke prime + 1.
48// See
49// https://lemire.me/blog/2019/02/08/faster-remainders-when-the-divisor-is-a-constant-beating-compilers-and-libdivide/
50static constexpr uint64_t kRadkePrimeInverts[] = {
51 0x281474976700001, 0xD5C26DD2255556, 0x5B9C78357DB6DC, 0x1745d1745d18,
52 0x84210842109, 0x456c797dd4a, 0x20408102041, 0x105197f7d74,
53 0x824a4e60b4, 0x4050647d9e, 0x202428adc4, 0x100501907e,
54 0x800400201, 0x401506e65, 0x200c44b25, 0x100110122};
55
56// Returns n % kRadkePrimes[idx].
57inline uint16_t fastmod(uint32_t n, int idx) {
58 uint64_t lowbits = (kRadkePrimeInverts[idx] * n) & 0x0000FFFFFFFFFFFF;
59 return (lowbits * kRadkePrimes[idx]) >> 48;
60}
61
62inline int initialCapacityIdx(uint16_t size_hint) {
63 uint32_t ht_len = (uint32_t)(((float)size_hint) / kMaxFillRatio) + 1;
64 for (int radkeIdx = 0; radkeIdx < 15; ++radkeIdx) {
65 if (kRadkePrimes[radkeIdx] >= ht_len) return radkeIdx;
66 }
67 return 15;
68}
69
70template <typename Key>
71struct DefaultHashFn : public std::hash<Key> {};
72
73template <>
74struct DefaultHashFn<::roo::string_view> {
75 inline size_t operator()(::roo::string_view val) const {
76 return murmur3_32(val.data(), val.size(), 0x92F4E42BUL);
77 }
78};
79
80template <>
81struct DefaultHashFn<std::string> {
82 inline size_t operator()(const std::string& val) const {
83 return DefaultHashFn<::roo::string_view>()(::roo::string_view(val));
84 }
85};
86
87template <>
88struct DefaultHashFn<const char*> {
89 inline size_t operator()(const char* val) const {
90 return DefaultHashFn<::roo::string_view>()(::roo::string_view(val));
91 }
92};
93
94template <size_t N>
96 size_t operator()(const SmallString<N>& str) const {
98 }
99};
100
101#ifdef ARDUINO
102template <>
103struct DefaultHashFn<::String> {
104 size_t operator()(::String val) const {
105 return DefaultHashFn<::roo::string_view>()(
106 ::roo::string_view(val.c_str(), val.length()));
107 }
108};
109#endif
110
112 // Required to denote a transparent hash.
113 using is_transparent = void;
114
115 // Hash operations required to be consistent:
116 // a == b => hash(a) == hash(b).
117
118 inline size_t operator()(const std::string& val) const {
119 return DefaultHashFn<std::string>()(val);
120 }
121 inline size_t operator()(const char* val) const {
122 return DefaultHashFn<const char*>()(val);
123 }
124 inline size_t operator()(::roo::string_view val) const {
126 }
127 template <size_t N>
128 inline size_t operator()(const SmallString<N>& val) const {
130 }
131
132#ifdef ARDUINO
133 inline size_t operator()(::String val) const {
135 }
136#endif
137};
138
140 // Required to denote a transparent comparator.
142
143 template <typename X, typename Y>
144 inline bool operator()(const X& x, const Y& y) const {
145 return x == y;
146 }
147};
148
149template <typename _Func, typename _SfinaeType, typename = std::void_t<>>
151
152template <typename _Func, typename _SfinaeType>
154 std::void_t<typename _Func::is_transparent>> {
155 typedef void type;
156};
157
158template <typename _Func, typename _SfinaeType>
161
162// For maps, where Key == Entry.
163template <typename Entry>
165 const Entry& operator()(const Entry& entry) const { return entry; }
166};
167
168/// @brief Flat, memory-conscious hash table optimized for small collections.
169///
170/// Uses open addressing with quadratic probing and stores entries in contiguous
171/// arrays for low overhead. Maximum supported size is approximately 64k
172/// elements.
173///
174/// @tparam Entry Stored entry type.
175/// @tparam Key Key type used for lookup.
176/// @tparam HashFn Hash function.
177/// @tparam KeyFn Extracts a key from an entry.
178/// @tparam KeyCmpFn Key equality predicate.
179template <typename Entry, typename Key, typename HashFn = DefaultHashFn<Key>,
180 typename KeyFn = DefaultKeyFn<Entry>,
181 typename KeyCmpFn = std::equal_to<Key>>
183 public:
184 /// @brief Constant forward iterator.
186 public:
187 using iterator_category = std::forward_iterator_tag;
188 using difference_type = std::ptrdiff_t;
189 using value_type = const Entry;
190 using pointer = const Entry*;
191 using reference = const Entry&;
192
194
195 const Entry& operator*() const { return ht_->buffer_[pos_]; }
196 const Entry* operator->() const { return &ht_->buffer_[pos_]; }
197
199 uint16_t ht_len = ht_->ht_len();
200 do {
201 ++pos_;
202 } while (pos_ < ht_len && ht_->states_[pos_] >= 0);
203 return *this;
204 }
205
207 ConstIterator itr = *this;
208 operator++();
209 return itr;
210 }
211
212 bool operator==(const ConstIterator& other) const {
213 return ht_ == other.ht_ && pos_ == other.pos_;
214 }
215
216 bool operator!=(const ConstIterator& other) const {
217 return ht_ != other.ht_ || pos_ != other.pos_;
218 }
219
220 private:
221 friend class FlatSmallHashtable;
222
226 : ht_(ht), pos_(pos) {}
227
229 uint16_t pos_;
230 };
231
232 /// @brief Mutable forward iterator.
233 class Iterator {
234 public:
235 using iterator_category = std::forward_iterator_tag;
236 using difference_type = std::ptrdiff_t;
238 using pointer = Entry*;
239 using reference = Entry&;
240
242
243 Entry& operator*() { return ht_->buffer_[pos_]; }
244 Entry* operator->() { return &ht_->buffer_[pos_]; }
245
246 operator ConstIterator() const { return ConstIterator(ht_, pos_); }
247
249 uint16_t ht_len = ht_->ht_len();
250 do {
251 ++pos_;
252 } while (pos_ < ht_len && ht_->states_[pos_] >= 0);
253 return *this;
254 }
255
257 Iterator itr = *this;
258 operator++();
259 return itr;
260 }
261
262 bool operator==(const Iterator& other) const {
263 return ht_ == other.ht_ && pos_ == other.pos_;
264 }
265
266 bool operator!=(const Iterator& other) const {
267 return ht_ != other.ht_ || pos_ != other.pos_;
268 }
269
270 private:
271 friend class FlatSmallHashtable;
272
275 : ht_(ht), pos_(pos) {}
276
278 uint16_t pos_;
279 };
280
281 using key_type = Key;
283 using hasher = HashFn;
287
288 /// @brief Constructs a table from an iterator range.
289 template <typename InputIt>
293 key_cmp_fn) {
294 for (auto it = first; it != last; ++it) {
295 insert(*it);
296 }
297 }
298
299 /// @brief Constructs a table from an initializer list.
305
306 /// @brief Constructs an empty table with default initial capacity.
310
311 /// @brief Constructs an empty table sized for `size_hint` elements.
312 /// @param size_hint Expected number of items.
315 : hash_fn_(hash_fn),
316 key_fn_(key_fn),
317 key_cmp_fn_(key_cmp_fn),
318 capacity_idx_(initialCapacityIdx(size_hint)),
319 used_(0),
320 erased_(0),
321 resize_threshold_(
322 capacity_idx_ == 15
323 ? 64000
324 : (uint16_t)(((float)kRadkePrimes[capacity_idx_]) *
326 buffer_(capacity_idx_ > 0 ? new Entry[kRadkePrimes[capacity_idx_]]
327 : nullptr),
328 states_(capacity_idx_ > 0 ? new State[kRadkePrimes[capacity_idx_]]
329 : &dummy_empty_state_) {
330 std::fill(&states_[0], &states_[kRadkePrimes[capacity_idx_]], EMPTY);
331 }
332
333 /// @brief Move constructor.
335 : hash_fn_(std::move(other.hash_fn_)),
336 key_fn_(std::move(other.key_fn_)),
337 key_cmp_fn_(std::move(other.key_cmp_fn_)),
338 capacity_idx_(other.capacity_idx_),
339 used_(other.used_),
340 erased_(other.erased_),
341 resize_threshold_(other.resize_threshold_),
342 buffer_(other.buffer_),
343 states_(capacity_idx_ > 0 ? other.states_ : &dummy_empty_state_) {
344 other.capacity_idx_ = 0;
345 other.used_ = 0;
346 other.erased_ = 0;
347 other.buffer_ = nullptr;
348 other.states_ = &dummy_empty_state_;
349 }
350
351 /// @brief Copy constructor.
353 : hash_fn_(other.hash_fn_),
354 key_fn_(other.key_fn_),
355 key_cmp_fn_(other.key_cmp_fn_),
356 capacity_idx_(other.capacity_idx_),
357 used_(other.used_),
358 erased_(other.erased_),
359 resize_threshold_(other.resize_threshold_),
360 buffer_(capacity_idx_ > 0 ? new Entry[kRadkePrimes[capacity_idx_]]
361 : nullptr),
362 states_(capacity_idx_ > 0 ? new State[kRadkePrimes[capacity_idx_]]
363 : &dummy_empty_state_) {
364 if (other.buffer_ != nullptr) {
365 std::copy(&other.buffer_[0], &other.buffer_[kRadkePrimes[capacity_idx_]],
366 &buffer_[0]);
367 }
368 std::copy(&other.states_[0], &other.states_[kRadkePrimes[capacity_idx_]],
369 &states_[0]);
370 }
371
372 /// @brief Destructor.
374 if (capacity_idx_ > 0) delete[] states_;
375 delete[] buffer_;
376 }
377
378 /// @brief Move assignment.
380 if (this != &other) {
381 if (capacity_idx_ > 0) delete[] states_;
382 delete[] buffer_;
383 hash_fn_ = std::move(other.hash_fn_);
384 key_fn_ = std::move(other.key_fn_);
385 key_cmp_fn_ = std::move(other.key_cmp_fn_);
386 capacity_idx_ = other.capacity_idx_;
387 used_ = other.used_;
388 erased_ = other.erased_;
389 resize_threshold_ = other.resize_threshold_;
390 if (capacity_idx_ > 0) {
391 buffer_ = other.buffer_;
392 states_ = other.states_;
393 other.capacity_idx_ = 0;
394 other.used_ = 0;
395 other.erased_ = 0;
396 other.buffer_ = nullptr;
397 other.states_ = &dummy_empty_state_;
398 } else {
399 buffer_ = nullptr;
400 states_ = &dummy_empty_state_;
401 }
402 }
403 return *this;
404 }
405
406 /// @brief Copy assignment.
408 if (this != &other) {
409 if (capacity_idx_ > 0) delete[] states_;
410 delete[] buffer_;
411 hash_fn_ = other.hash_fn_;
412 key_fn_ = other.key_fn_;
413 key_cmp_fn_ = other.key_cmp_fn_;
414 capacity_idx_ = other.capacity_idx_;
415 used_ = other.used_;
416 erased_ = other.erased_;
417 resize_threshold_ = other.resize_threshold_;
418 buffer_ =
419 capacity_idx_ > 0 ? new Entry[kRadkePrimes[capacity_idx_]] : nullptr;
420 states_ = capacity_idx_ > 0 ? new State[kRadkePrimes[capacity_idx_]]
421 : &dummy_empty_state_;
422 if (other.buffer_ != nullptr) {
423 std::copy(&other.buffer_[0],
424 &other.buffer_[kRadkePrimes[capacity_idx_]], &buffer_[0]);
425 }
426 std::copy(&other.states_[0], &other.states_[kRadkePrimes[capacity_idx_]],
427 &states_[0]);
428 }
429 return *this;
430 }
431
432 /// @brief Equality comparison by key/value content.
434 if (other.size() != size()) return false;
435 // Note: maps with different capacities or different insert/erase history
436 // may have different iteration order, thus we need to use lookup on one of
437 // them.
438 for (const auto& e : *this) {
439 auto itr = other.find(key_fn_(e));
440 if (itr == other.end()) return false;
441 if (*itr != e) return false;
442 }
443 return true;
444 }
445
446 bool operator!=(const FlatSmallHashtable& other) { return !(*this == other); }
447
448 /// @brief Returns the internal bucket array length.
449 uint16_t ht_len() const { return kRadkePrimes[capacity_idx_]; }
450
451 /// @brief Returns an iterator to the first element.
453 uint16_t cap = ht_len();
454 uint16_t pos = 0;
455 for (; pos < cap; ++pos) {
456 if (states_[pos] < 0) break;
457 }
458 return ConstIterator(this, pos);
459 }
460
461 /// @brief Returns a mutable iterator to the first element.
463 uint16_t cap = ht_len();
464 uint16_t pos = 0;
465 for (; pos < cap; ++pos) {
466 if (states_[pos] < 0) break;
467 }
468 return Iterator(this, pos);
469 }
470
471 /// @brief Returns iterator past the end.
472 Iterator end() { return Iterator(this, ht_len()); }
473
474 /// @brief Returns const iterator past the end.
475 ConstIterator end() const { return ConstIterator(this, ht_len()); }
476
477 /// @brief Returns the number of stored elements.
478 uint16_t size() const { return used_ - erased_; }
479
480 /// @brief Returns whether the table is empty.
481 bool empty() const { return used_ == erased_; }
482
483 /// @brief Returns the number of elements insertable before rehashing.
484 uint16_t capacity() const { return resize_threshold_; }
485
486 /// @brief Finds `key` and returns a const iterator to the matching entry.
487 /// @return `end()` when not found.
488 ConstIterator find(const Key& key) const {
489 size_t hash = hash_fn_(key);
490 const uint16_t pos = fastmod(hash, capacity_idx_);
491 if (states_[pos] == EMPTY) return end();
492 if (states_[pos] < 0 && (states_[pos] & 0x7F) == (hash & 0x7F) &&
493 key_cmp_fn_(key_fn_(buffer_[pos]), key)) {
494 return ConstIterator(this, pos);
495 }
496 const uint16_t cap = ht_len();
497 uint32_t p = pos;
498 p += (cap - 2);
499 int32_t j = 2 - cap;
500 while (true) {
501 if (p >= cap) p -= cap;
502 if (states_[p] == EMPTY) return end();
503 if (states_[p] < 0 && (states_[p] & 0x7F) == (hash & 0x7F) &&
504 key_cmp_fn_(key_fn_(buffer_[p]), key)) {
505 return ConstIterator(this, p);
506 }
507 j += 2;
508 assert(j < cap);
509 p += (j >= 0 ? j : -j);
510 }
511 }
512
513 /// @brief Heterogeneous lookup overload of `find`.
514 /// @return `end()` when not found.
515 template <typename K, typename = has_is_transparent_t<HashFn, K>,
516 typename = has_is_transparent_t<KeyCmpFn, K>>
517 ConstIterator find(const K& key) const {
518 size_t hash = hash_fn_(key);
519 const uint16_t pos = fastmod(hash, capacity_idx_);
520 if (states_[pos] == EMPTY) return end();
521 if (states_[pos] < 0 && (states_[pos] & 0x7F) == (hash & 0x7F) &&
522 key_cmp_fn_(key_fn_(buffer_[pos]), key)) {
523 return ConstIterator(this, pos);
524 }
525 const uint16_t cap = ht_len();
526 uint32_t p = pos;
527 p += (cap - 2);
528 int32_t j = 2 - cap;
529 while (true) {
530 if (p >= cap) p -= cap;
531 if (states_[p] == EMPTY) return end();
532 if (states_[p] < 0 && (states_[p] & 0x7F) == (hash & 0x7F) &&
533 key_cmp_fn_(key_fn_(buffer_[p]), key)) {
534 return ConstIterator(this, p);
535 }
536 j += 2;
537 assert(j < cap);
538 p += (j >= 0 ? j : -j);
539 }
540 }
541
542 /// @brief Removes an entry by key.
543 /// @return `true` if an entry was removed.
544 bool erase(const Key& key) {
545 size_t hash = hash_fn_(key);
546 const uint16_t pos = fastmod(hash, capacity_idx_);
547 if (states_[pos] == EMPTY) return false;
548 if (states_[pos] < 0 && (states_[pos] & 0x7F) == (hash & 0x7F) &&
549 key_cmp_fn_(key_fn_(buffer_[pos]), key)) {
550 buffer_[pos] = Entry();
551 if (used_ == 1 && erased_ == 0) {
552 // Fast path (fast-clear). It is safe to do because there was no
553 // rehashing. (It only works when used_ == 1, because otherwise the
554 // other items might have been rehashed away from this bucket).
555 states_[pos] = EMPTY;
556 --used_;
557 } else {
558 states_[pos] = DELETED;
559 ++erased_;
560 }
561 return true;
562 }
563 const uint16_t cap = ht_len();
564 uint32_t p = pos;
565 p += (cap - 2);
566 int32_t j = 2 - cap;
567 while (true) {
568 if (p >= cap) p -= cap;
569 if (states_[p] == EMPTY) return false;
570 if (states_[p] < 0 && (states_[p] & 0x7F) == (hash & 0x7F) &&
571 key_cmp_fn_(key_fn_(buffer_[p]), key)) {
572 states_[p] = DELETED;
573 buffer_[p] = Entry();
574 ++erased_;
575 return true;
576 }
577 j += 2;
578 assert(j < cap);
579 p += (j >= 0 ? j : -j);
580 }
581 }
582
583 /// @brief Heterogeneous key overload of `erase`.
584 /// @return `true` if an entry was removed.
585 template <typename K, typename = has_is_transparent_t<HashFn, K>,
586 typename = has_is_transparent_t<KeyCmpFn, K>>
587 bool erase(const K& key) {
588 size_t hash = hash_fn_(key);
589 const uint16_t pos = fastmod(hash, capacity_idx_);
590 if (states_[pos] == EMPTY) return false;
591 if (states_[pos] < 0 && (states_[pos] & 0x7F) == (hash & 0x7F) &&
592 key_cmp_fn_(key_fn_(buffer_[pos]), key)) {
593 states_[pos] = DELETED;
594 buffer_[pos] = Entry();
595 ++erased_;
596 return true;
597 }
598 const uint16_t cap = ht_len();
599 uint32_t p = pos;
600 p += (cap - 2);
601 int32_t j = 2 - cap;
602 while (true) {
603 if (p >= cap) p -= cap;
604 if (states_[p] == EMPTY) return false;
605 if (states_[p] < 0 && (states_[p] & 0x7F) == (hash & 0x7F) &&
606 key_cmp_fn_(key_fn_(buffer_[p]), key)) {
607 states_[p] = DELETED;
608 buffer_[p] = Entry();
609 ++erased_;
610 return true;
611 }
612 j += 2;
613 assert(j < cap);
614 p += (j >= 0 ? j : -j);
615 }
616 }
617
618 /// @brief Removes the entry at `itr` and returns iterator to the next entry.
620 if (itr == end()) return end();
621 Iterator next(this, itr.pos_);
622 ++next;
623 erase(key_fn_(*itr));
624 return next;
625 }
626
627 /// @brief Removes all entries while preserving current allocated capacity.
628 void clear() {
629 if (used_ == 0 && erased_ == 0) return;
630 std::fill(&states_[0], &states_[kRadkePrimes[capacity_idx_]], EMPTY);
631 for (size_t i = 0; i < kRadkePrimes[capacity_idx_]; ++i) {
632 buffer_[i] = Entry();
633 }
634 // Avoiding std::fill, because it doesn't work for non-copyable entries.
635 // std::fill(&buffer_[0], &buffer_[kRadkePrimes[capacity_idx_]], Entry());
636 used_ = 0;
637 erased_ = 0;
638 }
639
640 /// @brief Rebuilds the table to remove tombstones and shrink capacity.
641 void compact() {
643 if (capacity_idx == capacity_idx_ && erased_ == 0) return;
644 assert(capacity_idx < 15); // Or, exceeded maximum hashtable size.
646 size(), hash_fn_, key_fn_, key_cmp_fn_);
647 for (auto& e : *this) {
648 newt.insert(std::move(e));
649 }
650 *this = std::move(newt);
651 }
652
653 /// @brief Returns whether `key` exists in the table.
654 bool contains(const Key& key) const { return find(key).pos_ != ht_len(); }
655
656 /// @brief Heterogeneous key overload of `contains`.
657 template <typename K, typename = has_is_transparent_t<HashFn, K>,
658 typename = has_is_transparent_t<KeyCmpFn, K>>
659 bool contains(const K& key) const {
660 return find(key).pos_ != ht_len();
661 }
662
663 /// @brief Inserts `val` if key is not present.
664 /// @return Pair of iterator and insertion flag.
665 std::pair<Iterator, bool> insert(Entry val) {
666 Key key = key_fn_(val);
667 size_t hash = hash_fn_(key);
668 uint16_t pos = fastmod(hash, capacity_idx_);
669 // Fast path.
670 if (states_[pos] < 0 && (states_[pos] & 0x7F) == (hash & 0x7F) &&
671 key_cmp_fn_(key_fn_(buffer_[pos]), key)) {
672 return std::make_pair(Iterator(this, pos), false);
673 }
674 if (used_ >= resize_threshold_) {
675 if (empty() && erased_ > 0) {
676 // Clearing is faster than rehashing.
677 clear();
678 } else {
679 // Before rehashing see if maybe the entry is already in the hashtable.
681 if (itr != end()) {
682 return std::make_pair(itr, false);
683 }
684 // Need to rehash.
686 size() + 1, hash_fn_, key_fn_, key_cmp_fn_);
687 // Check if we didn't exceed the maximum hashtable size.
688 assert(newt.capacity() >= size() + 1);
689 for (auto& e : *this) {
690 newt.insert(std::move(e));
691 }
692 if (newt.capacity() == capacity()) {
693 // In this case, prefer to reuse the old storage, and release the new
694 // storage, because doing otherwise thrashes the heap. (Experimentally
695 // observed on ESP32).
696 used_ = newt.used_;
697 erased_ = 0;
698 memcpy(states_, newt.states_,
699 kRadkePrimes[capacity_idx_] * sizeof(State));
700 for (size_t i = 0; i < kRadkePrimes[capacity_idx_]; ++i) {
701 buffer_[i] = std::move(newt.buffer_[i]);
702 }
703 } else {
704 *this = std::move(newt);
705 }
706 }
707 pos = fastmod(hash, capacity_idx_);
708 }
709 // Fast path for not found.
710 if (states_[pos] == EMPTY) {
711 states_[pos] = (hash & 0x7F) | 0x80;
712 buffer_[pos] = std::move(val);
713 ++used_;
714 return std::make_pair(Iterator(this, pos), true);
715 }
716 const uint16_t cap = ht_len();
717 uint32_t p = pos;
718 p += (cap - 2);
719 int32_t j = 2 - cap;
720 while (true) {
721 if (p >= cap) p -= cap;
722 if (states_[p] == EMPTY) {
723 // We can insert here.
724 states_[p] = (hash & 0x7F) | 0x80;
725 buffer_[p] = std::move(val);
726 ++used_;
727 return std::make_pair(Iterator(this, p), true);
728 }
729 if (states_[p] < 0 && (states_[p] & 0x7F) == (hash & 0x7F) &&
730 key_cmp_fn_(key_fn_(buffer_[p]), key)) {
731 return std::make_pair(Iterator(this, p), false);
732 }
733 j += 2;
734 assert(j < cap);
735 p += (j >= 0 ? j : -j);
736 }
737 }
738
739 protected:
741 size_t hash = hash_fn_(key);
742 const uint16_t pos = fastmod(hash, capacity_idx_);
743 if (states_[pos] == EMPTY) return end();
744 if (states_[pos] < 0 && (states_[pos] & 0x7F) == (hash & 0x7F) &&
745 key_cmp_fn_(key_fn_(buffer_[pos]), key)) {
746 return Iterator(this, pos);
747 }
748 const uint16_t cap = ht_len();
749 uint32_t p = pos;
750 p += (cap - 2);
751 int32_t j = 2 - cap;
752 while (true) {
753 if (p >= cap) p -= cap;
754 if (states_[p] == EMPTY) return end();
755 if (states_[p] < 0 && (states_[p] & 0x7F) == (hash & 0x7F) &&
756 key_cmp_fn_(key_fn_(buffer_[p]), key)) {
757 return Iterator(this, p);
758 }
759 j += 2;
760 assert(j < cap);
761 p += (j >= 0 ? j : -j);
762 }
763 }
764
765 template <typename K, typename = has_is_transparent_t<HashFn, K>,
766 typename = has_is_transparent_t<KeyCmpFn, K>>
768 size_t hash = hash_fn_(key);
769 const uint16_t pos = fastmod(hash, capacity_idx_);
770 if (states_[pos] == EMPTY) return end();
771 if (states_[pos] < 0 && (states_[pos] & 0x7F) == (hash & 0x7F) &&
772 key_cmp_fn_(key_fn_(buffer_[pos]), key)) {
773 return Iterator(this, pos);
774 }
775 const uint16_t cap = ht_len();
776 uint32_t p = pos;
777 p += (cap - 2);
778 int32_t j = 2 - cap;
779 while (true) {
780 if (p >= cap) p -= cap;
781 if (states_[p] == EMPTY) return end();
782 if (states_[p] < 0 && (states_[p] & 0x7F) == (hash & 0x7F) &&
783 key_cmp_fn_(key_fn_(buffer_[p]), key)) {
784 return Iterator(this, p);
785 }
786 j += 2;
787 assert(j < cap);
788 p += (j >= 0 ? j : -j);
789 }
790 }
791
792 private:
793 using State = int8_t;
794 static constexpr State EMPTY = 0;
795 static constexpr State DELETED = 1;
796 // Full items are marked with a bit pattern of the form 0x80 + (hash & 0x7F).
797
798 friend class ConstIterator;
799 friend class Iterator;
800
801 HashFn hash_fn_;
802 KeyFn key_fn_;
803 KeyCmpFn key_cmp_fn_;
804 int capacity_idx_;
805 uint16_t used_;
806 uint16_t erased_;
807 uint16_t resize_threshold_;
808 Entry* buffer_;
809 State* states_;
810 State dummy_empty_state_;
811};
812
813} // namespace roo_collections
bool operator!=(const ConstIterator &other) const
bool operator==(const ConstIterator &other) const
Flat, memory-conscious hash table optimized for small collections.
std::pair< Iterator, bool > insert(Entry val)
Inserts val if key is not present.
bool operator!=(const FlatSmallHashtable &other)
bool contains(const Key &key) const
Returns whether key exists in the table.
Iterator erase(const ConstIterator &itr)
Removes the entry at itr and returns iterator to the next entry.
Iterator end()
Returns iterator past the end.
FlatSmallHashtable(InputIt first, InputIt last, HashFn hash_fn=HashFn(), KeyFn key_fn=KeyFn(), KeyCmpFn key_cmp_fn=KeyCmpFn())
Constructs a table from an iterator range.
bool empty() const
Returns whether the table is empty.
FlatSmallHashtable(FlatSmallHashtable &&other)
Move constructor.
FlatSmallHashtable & operator=(const FlatSmallHashtable &other)
Copy assignment.
FlatSmallHashtable(uint16_t size_hint, HashFn hash_fn=HashFn(), KeyFn key_fn=KeyFn(), KeyCmpFn key_cmp_fn=KeyCmpFn())
Constructs an empty table sized for size_hint elements.
FlatSmallHashtable & operator=(FlatSmallHashtable &&other)
Move assignment.
uint16_t ht_len() const
Returns the internal bucket array length.
bool erase(const K &key)
Heterogeneous key overload of erase.
uint16_t capacity() const
Returns the number of elements insertable before rehashing.
void clear()
Removes all entries while preserving current allocated capacity.
FlatSmallHashtable(std::initializer_list< Entry > init, HashFn hash_fn=HashFn(), KeyFn key_fn=KeyFn(), KeyCmpFn key_cmp_fn=KeyCmpFn())
Constructs a table from an initializer list.
bool erase(const Key &key)
Removes an entry by key.
FlatSmallHashtable(HashFn hash_fn=HashFn(), KeyFn key_fn=KeyFn(), KeyCmpFn key_cmp_fn=KeyCmpFn())
Constructs an empty table with default initial capacity.
uint16_t size() const
Returns the number of stored elements.
bool operator==(const FlatSmallHashtable &other) const
Equality comparison by key/value content.
void compact()
Rebuilds the table to remove tombstones and shrink capacity.
FlatSmallHashtable(const FlatSmallHashtable &other)
Copy constructor.
ConstIterator end() const
Returns const iterator past the end.
bool contains(const K &key) const
Heterogeneous key overload of contains.
ConstIterator begin() const
Returns an iterator to the first element.
ConstIterator find(const Key &key) const
Finds key and returns a const iterator to the matching entry.
Iterator begin()
Returns a mutable iterator to the first element.
ConstIterator find(const K &key) const
Heterogeneous lookup overload of find.
Fixed-capacity string stored inline (no heap allocation).
Hashing utilities used by roo_collections containers.
static constexpr uint64_t kRadkePrimeInverts[]
int initialCapacityIdx(uint16_t size_hint)
static constexpr uint16_t kRadkePrimes[]
uint16_t fastmod(uint32_t n, int idx)
uint32_t murmur3_32(const void *key, size_t len, uint32_t seed)
Computes 32-bit MurmurHash3 of a binary buffer.
Definition hash.cpp:12
static constexpr float kMaxFillRatio
typename has_is_transparent< _Func, _SfinaeType >::type has_is_transparent_t
Fixed-capacity string utility type.
size_t operator()(const SmallString< N > &str) const
size_t operator()(const std::string &val) const
const Entry & operator()(const Entry &entry) const
bool operator()(const X &x, const Y &y) const
size_t operator()(const std::string &val) const
size_t operator()(const SmallString< N > &val) const
size_t operator()(::roo::string_view val) const