// SPDX-License-Identifier: BSD-2-Clause // author: Max Kellermann #pragma once #include "Concepts.hxx" #include "IntrusiveList.hxx" #include // for std::all_of() #include #include // for std::regular_invocable #include // for std::accumulate() template concept IntrusiveHashSetOperatorsConcept = requires(const Operators &ops, const Item &c, const Item &c2) { { ops.get_key(c) } noexcept; /* note: no "noexcept" here because std::hash is not noexcept */ { ops.hash(ops.get_key(c)) } -> std::same_as; /* note: no "noexcept" here because std::equal_to is not noexcept */ { ops.equal(ops.get_key(c), ops.get_key(c2)) } -> std::same_as; }; struct IntrusiveHashSetOptions { bool constant_time_size = false; /** * @see IntrusiveListOptions::zero_initialized */ bool zero_initialized = false; }; /** * @param Tag an arbitrary tag type to allow using multiple base hooks */ template struct IntrusiveHashSetHook { using SiblingsHook = IntrusiveListHook; SiblingsHook intrusive_hash_set_siblings; void unlink() noexcept { intrusive_hash_set_siblings.unlink(); } bool is_linked() const noexcept { return intrusive_hash_set_siblings.is_linked(); } }; /** * For classes which embed #IntrusiveHashSetHook as base class. * * @param Tag selector for which #IntrusiveHashSetHook to use */ template struct IntrusiveHashSetBaseHookTraits { /* a never-called helper function which is used by _Cast() */ template static constexpr IntrusiveHashSetHook _Identity(const IntrusiveHashSetHook &) noexcept; /* another never-called helper function which "calls" _Identity(), implicitly casting the item to the IntrusiveHashSetHook specialization; we use this to detect which IntrusiveHashSetHook specialization is used */ template static constexpr auto _Cast(const U &u) noexcept { return decltype(_Identity(u))(); } template using Hook = decltype(_Cast(std::declval())); static constexpr T *Cast(Hook *node) noexcept { return static_cast(node); } static constexpr auto &ToHook(T &t) noexcept { return static_cast &>(t); } }; /** * For classes which embed #IntrusiveListHook as member. */ template struct IntrusiveHashSetMemberHookTraits { using T = MemberPointerContainerType; using _Hook = MemberPointerType; template using Hook = _Hook; static constexpr T *Cast(Hook *node) noexcept { return &ContainerCast(*node, member); } static constexpr auto &ToHook(T &t) noexcept { return t.*member; } }; /** * @param GetKey a function object which extracts the "key" part of an * item */ template GetKey, std::regular_invocable> Hash, std::predicate, std::invoke_result_t> Equal> struct IntrusiveHashSetOperators { using hasher = Hash; using key_equal = Equal; [[no_unique_address]] Hash hash; [[no_unique_address]] Equal equal; [[no_unique_address]] GetKey get_key; }; /** * A hash table implementation which stores pointers to items which * have an embedded #IntrusiveHashSetHook. The actual table is * embedded with a compile-time fixed size in this object. * * @param Operators a class which contains functions `hash` and * `equal` */ template Operators, typename HookTraits=IntrusiveHashSetBaseHookTraits, IntrusiveHashSetOptions options=IntrusiveHashSetOptions{}> class IntrusiveHashSet { static constexpr bool constant_time_size = options.constant_time_size; [[no_unique_address]] OptionalCounter counter; [[no_unique_address]] Operators ops; struct BucketHookTraits { template using HashSetHook = typename HookTraits::template Hook; template using ListHook = IntrusiveListMemberHookTraits<&HashSetHook::intrusive_hash_set_siblings>; template using Hook = typename HashSetHook::SiblingsHook; static constexpr T *Cast(IntrusiveListNode *node) noexcept { auto *hook = ListHook::Cast(node); return HookTraits::Cast(hook); } static constexpr auto &ToHook(T &t) noexcept { auto &hook = HookTraits::ToHook(t); return hook.intrusive_hash_set_siblings; } }; using Bucket = IntrusiveList; std::array table; using bucket_iterator = typename Bucket::iterator; using const_bucket_iterator = typename Bucket::const_iterator; public: using value_type = T; using reference = T &; using const_reference = const T &; using pointer = T *; using const_pointer = const T *; using size_type = std::size_t; using hasher = typename Operators::hasher; using key_equal = typename Operators::key_equal; [[nodiscard]] IntrusiveHashSet() noexcept = default; [[nodiscard]] constexpr const hasher &hash_function() const noexcept { return ops.hash; } [[nodiscard]] constexpr const key_equal &key_eq() const noexcept { return ops.equal; } [[nodiscard]] constexpr bool empty() const noexcept { if constexpr (constant_time_size) return size() == 0; else return std::all_of(table.begin(), table.end(), [](const auto &bucket){ return bucket.empty(); }); } [[nodiscard]] constexpr size_type size() const noexcept { if constexpr (constant_time_size) return counter; else return std::accumulate(table.begin(), table.end(), size_type{}, [](std::size_t n, const auto &bucket){ return n + bucket.size(); }); } constexpr void clear() noexcept { for (auto &i : table) i.clear(); counter.reset(); } constexpr void clear_and_dispose(Disposer auto disposer) noexcept { for (auto &i : table) i.clear_and_dispose(disposer); counter.reset(); } /** * Remove and dispose all items matching the given predicate. * * @return the number of removed items */ std::size_t remove_and_dispose_if(std::predicate auto pred, Disposer auto disposer) noexcept { std::size_t n = 0; for (auto &bucket : table) n += bucket.remove_and_dispose_if(pred, disposer); counter -= n; return n; } /** * Remove and dispose all items with the specified key. * * @return the number of removed items */ constexpr std::size_t remove_and_dispose_key(const auto &key, Disposer auto disposer) noexcept { auto &bucket = GetBucket(key); std::size_t n = bucket.remove_and_dispose_if([this, &key](const auto &item){ return ops.equal(key, ops.get_key(item)); }, disposer); counter -= n; return n; } constexpr std::size_t remove_and_dispose_key_if(const auto &key, std::predicate auto pred, Disposer auto disposer) noexcept { auto &bucket = GetBucket(key); std::size_t n = bucket.remove_and_dispose_if([this, &key, &pred](const auto &item){ return ops.equal(key, ops.get_key(item)) && pred(item); }, disposer); counter -= n; return n; } [[nodiscard]] static constexpr bucket_iterator iterator_to(reference item) noexcept { return Bucket::iterator_to(item); } /** * Prepare insertion of a new item. If the key already * exists, return an iterator to the existing item and * `false`. If the key does not exist, return an iterator to * the bucket where the new item may be inserted using * insert() and `true`. */ [[nodiscard]] [[gnu::pure]] constexpr std::pair insert_check(const auto &key) noexcept { auto &bucket = GetBucket(key); for (auto &i : bucket) if (ops.equal(key, ops.get_key(i))) return {bucket.iterator_to(i), false}; /* bucket.end() is a pointer to the bucket's list head, a stable value that is guaranteed to be still valid when insert_commit() gets called eventually */ return {bucket.end(), true}; } /** * Like insert_check(), but existing items are only considered * conflicting if they match the given predicate. */ [[nodiscard]] [[gnu::pure]] constexpr std::pair insert_check_if(const auto &key, std::predicate auto pred) noexcept { auto &bucket = GetBucket(key); for (auto &i : bucket) if (ops.equal(key, ops.get_key(i)) && pred(i)) return {bucket.iterator_to(i), false}; /* bucket.end() is a pointer to the bucket's list head, a stable value that is guaranteed to be still valid when insert_commit() gets called eventually */ return {bucket.end(), true}; } /** * Finish the insertion if insert_check() has returned true. * * @param bucket the bucket returned by insert_check() */ constexpr bucket_iterator insert_commit(bucket_iterator bucket, reference item) noexcept { ++counter; /* using insert_after() so the new item gets inserted at the front of the bucket list */ return GetBucket(ops.get_key(item)).insert_after(bucket, item); } /** * Insert a new item without checking whether the key already * exists. */ constexpr bucket_iterator insert(reference item) noexcept { ++counter; return GetBucket(ops.get_key(item)).push_front(item); } constexpr bucket_iterator erase(bucket_iterator i) noexcept { --counter; return GetBucket(ops.get_key(*i)).erase(i); } constexpr bucket_iterator erase_and_dispose(bucket_iterator i, Disposer auto disposer) noexcept { auto result = erase(i); disposer(&*i); return result; } [[nodiscard]] [[gnu::pure]] constexpr bucket_iterator find(const auto &key) noexcept { auto &bucket = GetBucket(key); for (auto &i : bucket) if (ops.equal(key, ops.get_key(i))) return bucket.iterator_to(i); return end(); } [[nodiscard]] [[gnu::pure]] constexpr const_bucket_iterator find(const auto &key) const noexcept { auto &bucket = GetBucket(key); for (auto &i : bucket) if (ops.equal(key, ops.get_key(i))) return bucket.iterator_to(i); return end(); } /** * Like find(), but returns an item that matches the given * predicate. This is useful if the container can contain * multiple items that compare equal (according to #Equal, but * not according to #pred). */ [[nodiscard]] [[gnu::pure]] constexpr bucket_iterator find_if(const auto &key, std::predicate auto pred) noexcept { auto &bucket = GetBucket(key); for (auto &i : bucket) if (ops.equal(key, ops.get_key(i)) && pred(i)) return bucket.iterator_to(i); return end(); } /** * Like find_if(), but while traversing the bucket linked * list, remove and dispose expired items. * * @param expired_pred returns true if an item is expired; it * will be removed and disposed * * @param disposer function which will be called for items * that were removed (because they are expired) * * @param match_pred returns true if the desired item was * found */ [[nodiscard]] [[gnu::pure]] constexpr bucket_iterator expire_find_if(const auto &key, std::predicate auto expired_pred, Disposer auto disposer, std::predicate auto match_pred) noexcept { auto &bucket = GetBucket(key); for (auto i = bucket.begin(), e = bucket.end(); i != e;) { if (!ops.equal(key, ops.get_key(*i))) ++i; else if (expired_pred(*i)) i = erase_and_dispose(i, disposer); else if (match_pred(*i)) return i; else ++i; } return end(); } constexpr bucket_iterator end() noexcept { return table.front().end(); } constexpr const_bucket_iterator end() const noexcept { return table.front().end(); } constexpr void for_each(auto &&f) { for (auto &bucket : table) for (auto &i : bucket) f(i); } constexpr void for_each(auto &&f) const { for (const auto &bucket : table) for (const auto &i : bucket) f(i); } private: template [[gnu::pure]] [[nodiscard]] constexpr auto &GetBucket(K &&key) noexcept { const auto h = ops.hash(std::forward(key)); return table[h % table_size]; } template [[gnu::pure]] [[nodiscard]] constexpr const auto &GetBucket(K &&key) const noexcept { const auto h = ops.hash(std::forward(key)); return table[h % table_size]; } };