diff --git a/base/algorithm.h b/base/algorithm.h index 811ddfaf..497fa850 100644 --- a/base/algorithm.h +++ b/base/algorithm.h @@ -41,6 +41,28 @@ inline OutputIt Copy(InputIt first, InputIt last, OutputIt dest) { return dest; } +// ── Prev / Next ───────────────────────────────────────────────────── +// The iterator before / after `it`, which is left alone. +template +inline Iter Prev(Iter it) { + --it; + return it; +} +template +inline Iter Next(Iter it) { + ++it; + return it; +} + +// ── CopyN ─────────────────────────────────────────────────────────── +template +inline OutputIt CopyN(InputIt first, mem_size count, OutputIt dest) { + for (; count; --count, ++first, ++dest) { + *dest = *first; + } + return dest; +} + // ── Find ──────────────────────────────────────────────────────────── template inline Iter Find(Iter first, Iter last, const T& value) { @@ -68,6 +90,12 @@ inline bool AnyOf(Iter first, Iter last, Pred pred) { return false; } +// ── NoneOf ────────────────────────────────────────────────────────── +template +inline bool NoneOf(Iter first, Iter last, Pred pred) { + return !AnyOf(first, last, pred); +} + // ── AllOf ─────────────────────────────────────────────────────────── template inline bool AllOf(Iter first, Iter last, Pred pred) { @@ -280,14 +308,29 @@ inline T* Unique(T* first, T* last) { // ── EraseIf (container-level erase-remove) ────────────────────────── // Drops every element matching `pred` and returns how many went. Takes the // container, not a range, so the caller does not hand-write erase(remove_if()). +// Contiguous containers compact in one pass; node containers (maps, sets) +// erase element by element, handing `pred` each element as iteration does. template inline mem_size EraseIf(TContainer& container, Pred pred) { - auto* first = container.begin(); - auto* last = container.end(); - auto* kept = base::RemoveIf(first, last, pred); - const mem_size removed = static_cast(last - kept); - if (removed > 0) container.erase(kept, last); - return removed; + if constexpr (base::is_pointer_v) { + auto* first = container.begin(); + auto* last = container.end(); + auto* kept = base::RemoveIf(first, last, pred); + const mem_size removed = static_cast(last - kept); + if (removed > 0) container.erase(kept, last); + return removed; + } else { + mem_size removed = 0; + for (auto it = container.begin(); it != container.end();) { + if (pred(*it)) { + it = container.erase(it); + removed++; + } else { + ++it; + } + } + return removed; + } } // ── LowerBound (binary search) ────────────────────────────────────── diff --git a/base/algorithm_test.cc b/base/algorithm_test.cc index bddda19c..f5392557 100644 --- a/base/algorithm_test.cc +++ b/base/algorithm_test.cc @@ -3,6 +3,12 @@ #include +#include +#include +#include +#include +#include + #include #include @@ -82,4 +88,56 @@ TEST(AlgorithmBounds, UpperBoundPlain) { EXPECT_EQ(base::UpperBound(v, v + 5, 0), v); EXPECT_EQ(base::UpperBound(v, v + 5, 5), v + 5); } +TEST(AlgorithmAdditions, CopyNAndNoneOf) { + int src[4] = {1, 2, 3, 4}; + int dst[4] = {}; + EXPECT_EQ(base::CopyN(src, 3, dst), dst + 3); + EXPECT_EQ(dst[2], 3); + EXPECT_EQ(dst[3], 0); + EXPECT_TRUE(base::NoneOf(src, src + 4, [](int v) { return v > 4; })); + EXPECT_FALSE(base::NoneOf(src, src + 4, [](int v) { return v == 2; })); +} + +TEST(AlgorithmAdditions, EraseIfOnANodeContainer) { + base::HashMap map; + for (int i = 0; i < 10; i++) + map[i] = i; + EXPECT_EQ(base::EraseIf(map, [](const auto& kv) { return kv.second % 3 == 0; }), + 4u); + EXPECT_EQ(map.size(), 6u); + EXPECT_FALSE(map.contains(3)); + base::Vector vec{1, 2, 3, 4}; + EXPECT_EQ(base::EraseIf(vec, [](int v) { return v % 2 == 0; }), 2u); + EXPECT_EQ(vec.size(), 2u); +} + +TEST(AlgorithmAdditions, MinMaxOfAList) { + EXPECT_EQ(base::Min({5, 2, 9}), 2); + EXPECT_EQ(base::Max({5, 2, 9}), 9); + EXPECT_EQ(base::Max({1, 7}), 7u); +} + +TEST(AlgorithmAdditions, ArrayAndVectorOrderLexicographically) { + base::Array a{1, 2, 3}, b{1, 3, 0}; + EXPECT_TRUE(a < b); + EXPECT_FALSE(b < a); + EXPECT_FALSE(a < a); + base::Vector x{1, 2}, y{1, 2, 0}; + EXPECT_TRUE(x < y); + EXPECT_FALSE(y < x); + EXPECT_TRUE((base::Vector{0, 9} < base::Vector{1})); +} + +TEST(AlgorithmAdditions, PrevAndNextLeaveTheIteratorAlone) { + base::Map map; + map[1] = 10; + map[2] = 20; + auto it = map.find(2); + EXPECT_EQ(base::Prev(it)->first, 1); + EXPECT_EQ(it->first, 2); + EXPECT_TRUE(base::Next(it) == map.end()); + int v[3] = {1, 2, 3}; + EXPECT_EQ(*base::Next(v), 2); +} + } // namespace diff --git a/base/containers/array.h b/base/containers/array.h index a790bf7d..d042b43a 100644 --- a/base/containers/array.h +++ b/base/containers/array.h @@ -80,12 +80,21 @@ class Array { constexpr bool operator!=(const Array& other) const { return !(*this == other); } + // Lexicographic, so an Array can key an ordered container. + constexpr bool operator<(const Array& other) const { + for (mem_size i = 0; i < N; ++i) { + if (storage_[i] < other.storage_[i]) return true; + if (other.storage_[i] < storage_[i]) return false; + } + return false; + } + private: Storage storage_{}; }; template -mem_size ArraySize(T (&)[N]) { +constexpr mem_size ArraySize(const T (&)[N]) noexcept { return N; } } // namespace base diff --git a/base/containers/array_test.cc b/base/containers/array_test.cc index eef59527..36ee93ed 100644 --- a/base/containers/array_test.cc +++ b/base/containers/array_test.cc @@ -79,4 +79,13 @@ TEST(ArrayTest, DataAccessors) { EXPECT_EQ(arr.back(), 30); } -} // namespace base \ No newline at end of file +TEST(ArrayTest, ArraySizeIsConstant) { + static constexpr int values[7] = {}; + const char names[3][4] = {"a", "b", "c"}; + static_assert(ArraySize(values) == 7); + Array doubled; + EXPECT_EQ(doubled.size(), 14u); + EXPECT_EQ(ArraySize(names), 3u); +} + +} // namespace base diff --git a/base/containers/hash_map.h b/base/containers/hash_map.h new file mode 100644 index 00000000..2ba5fc7a --- /dev/null +++ b/base/containers/hash_map.h @@ -0,0 +1,420 @@ +// Copyright (C) 2026 Vincent Hengel. +// For licensing information see LICENSE at the root of this distribution. +// +// base::HashMap / base::HashSet - chained hash containers with the +// std::unordered_map / std::unordered_set contract: every element is a node +// of its own, so references and pointers to elements stay valid across +// inserts and rehashes, and iterators across everything but erasing the +// element they name. find/insert/emplace/erase(iterator) are std-shaped. +// +// Reach for base::UnorderedMap instead when element stability is not needed: +// open addressing keeps the elements inline and is faster to probe. +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace base { + +template , + class TEqual = Equal, + class TAllocator = DefaultAllocator> +class HashMap { + public: + using key_type = K; + using mapped_type = V; + using value_type = Pair; + + private: + struct Node { + // The pair is built in place, so a value that cannot move still fits. + template + Node(mem_size h, const K& key, TArgs&&... args) + : value{key, V(base::forward(args)...)}, hash(h) {} + value_type value; + Node* next = nullptr; + mem_size hash; + }; + + public: + template + class IteratorBase { + public: + using reference = conditional_t; + using pointer = conditional_t; + + IteratorBase() = default; + IteratorBase(Node* node, const HashMap* map) : node_(node), map_(map) {} + template + requires(kConst && !kOther) + IteratorBase(const IteratorBase& other) + : node_(other.node_), map_(other.map_) {} + + reference operator*() const { return node_->value; } + pointer operator->() const { return &node_->value; } + + IteratorBase& operator++() { + node_ = map_->NextNode(node_); + return *this; + } + IteratorBase operator++(int) { + IteratorBase before = *this; + node_ = map_->NextNode(node_); + return before; + } + + bool operator==(const IteratorBase& other) const { return node_ == other.node_; } + bool operator!=(const IteratorBase& other) const { return node_ != other.node_; } + + private: + friend class HashMap; + template + friend class IteratorBase; + Node* node_ = nullptr; + const HashMap* map_ = nullptr; + }; + using iterator = IteratorBase; + using const_iterator = IteratorBase; + + HashMap() = default; + HashMap(std::initializer_list> entries) { + reserve(entries.size()); + for (const auto& entry : entries) + emplace(entry.first, entry.second); + } + HashMap(const HashMap& other) : hasher_(other.hasher_), equal_(other.equal_) { + CopyFrom(other); + } + HashMap(HashMap&& other) noexcept { StealFrom(other); } + HashMap& operator=(const HashMap& other) { + if (this != &other) { + Release(); + hasher_ = other.hasher_; + equal_ = other.equal_; + CopyFrom(other); + } + return *this; + } + HashMap& operator=(HashMap&& other) noexcept { + if (this != &other) { + Release(); + StealFrom(other); + } + return *this; + } + ~HashMap() { Release(); } + + iterator begin() { return iterator(FirstNode(), this); } + iterator end() { return iterator(nullptr, this); } + const_iterator begin() const { return const_iterator(FirstNode(), this); } + const_iterator end() const { return const_iterator(nullptr, this); } + + [[nodiscard]] mem_size size() const { return size_; } + [[nodiscard]] bool empty() const { return size_ == 0; } + [[nodiscard]] mem_size bucket_count() const { return bucket_count_; } + + iterator find(const K& key) { return iterator(FindNode(key), this); } + const_iterator find(const K& key) const { + return const_iterator(FindNode(key), this); + } + [[nodiscard]] bool contains(const K& key) const { return FindNode(key) != nullptr; } + [[nodiscard]] mem_size count(const K& key) const { return contains(key) ? 1 : 0; } + + // Constructs the value from |args| when |key| is absent; an existing entry + // is left alone and the arguments are not consumed. + template + Pair emplace(const K& key, TArgs&&... args) { + const mem_size h = hasher_(key); + if (Node* node = FindNode(key, h)) + return {iterator(node, this), false}; + return {iterator(Link(h, key, base::forward(args)...), this), true}; + } + template + Pair try_emplace(const K& key, TArgs&&... args) { + return emplace(key, base::forward(args)...); + } + + template + Pair insert(const Pair& entry) { + return emplace(entry.first, entry.second); + } + template + Pair insert(Pair&& entry) { + return emplace(entry.first, base::move(entry.second)); + } + + // Replaces the value of an existing entry. + template + Pair insert_or_assign(const K& key, VV&& value) { + if (Node* node = FindNode(key)) { + node->value.second = base::forward(value); + return {iterator(node, this), false}; + } + return emplace(key, base::forward(value)); + } + + V& operator[](const K& key) { return emplace(key).first->second; } + + V& at(const K& key) { + Node* node = FindNode(key); + BASE_BUGCHECK(node != nullptr, "HashMap::at: key not present"); + return node->value.second; + } + const V& at(const K& key) const { + return const_cast(this)->at(key); + } + + mem_size erase(const K& key) { + if (!bucket_count_) + return 0; + const mem_size h = hasher_(key); + Node** link = &buckets_[BucketOf(h)]; + for (Node* node = *link; node; link = &node->next, node = node->next) { + if (node->hash == h && equal_(node->value.first, key)) { + *link = node->next; + Destroy(node); + return 1; + } + } + return 0; + } + + // Returns the element after the erased one. + iterator erase(const_iterator it) { + Node* victim = it.node_; + Node* next = NextNode(victim); + Node** link = &buckets_[BucketOf(victim->hash)]; + while (*link != victim) + link = &(*link)->next; + *link = victim->next; + Destroy(victim); + return iterator(next, this); + } + iterator erase(iterator it) { return erase(const_iterator(it)); } + + void clear() { + for (mem_size b = 0; b < bucket_count_; b++) { + for (Node* node = buckets_[b]; node;) { + Node* next = node->next; + node->~Node(); + TAllocator::Free(node, sizeof(Node)); + node = next; + } + buckets_[b] = nullptr; + } + size_ = 0; + } + + // Room for |count| elements without a rehash. + void reserve(mem_size count) { + if (count > bucket_count_) + Rehash(count); + } + + private: + static mem_size RoundUpToPowerOfTwo(mem_size n) { + mem_size p = 16; + while (p < n) + p *= 2; + return p; + } + + // Fibonacci hashing: the top bits of a multiplicative mix, so a hasher that + // leaves structure in its low bits (an identity hash of aligned addresses) + // still spreads over every bucket. + mem_size BucketOf(mem_size h) const { + return static_cast((static_cast(h) * 0x9E3779B97F4A7C15ull) >> + (64 - bucket_shift_)); + } + + Node* FindNode(const K& key) const { + return bucket_count_ ? FindNode(key, hasher_(key)) : nullptr; + } + Node* FindNode(const K& key, mem_size h) const { + if (!bucket_count_) + return nullptr; + for (Node* node = buckets_[BucketOf(h)]; node; node = node->next) + if (node->hash == h && equal_(node->value.first, key)) + return node; + return nullptr; + } + + Node* FirstNode() const { + for (mem_size b = 0; b < bucket_count_; b++) + if (buckets_[b]) + return buckets_[b]; + return nullptr; + } + + Node* NextNode(Node* node) const { + if (node->next) + return node->next; + for (mem_size b = BucketOf(node->hash) + 1; b < bucket_count_; b++) + if (buckets_[b]) + return buckets_[b]; + return nullptr; + } + + template + Node* Link(mem_size h, const K& key, TArgs&&... args) { + if (size_ + 1 > bucket_count_) + Rehash(size_ + 1); + void* memory = TAllocator::Allocate(sizeof(Node)); + BASE_FATAL_CHECK(memory, "HashMap: node allocation failed"); + Node* node = ::new (memory) Node(h, key, base::forward(args)...); + Node*& head = buckets_[BucketOf(h)]; + node->next = head; + head = node; + size_++; + return node; + } + + void Destroy(Node* node) { + node->~Node(); + TAllocator::Free(node, sizeof(Node)); + size_--; + } + + void Rehash(mem_size wanted) { + const mem_size count = RoundUpToPowerOfTwo(wanted); + u32 shift = 0; + while ((mem_size(1) << shift) < count) + shift++; + auto* fresh = static_cast(TAllocator::Allocate(count * sizeof(Node*))); + BASE_FATAL_CHECK(fresh, "HashMap: bucket allocation failed"); + for (mem_size b = 0; b < count; b++) + fresh[b] = nullptr; + Node** old = buckets_; + const mem_size old_count = bucket_count_; + buckets_ = fresh; + bucket_count_ = count; + bucket_shift_ = shift; + for (mem_size b = 0; b < old_count; b++) { + for (Node* node = old[b]; node;) { + Node* next = node->next; + Node*& head = buckets_[BucketOf(node->hash)]; + node->next = head; + head = node; + node = next; + } + } + if (old) + TAllocator::Free(old, old_count * sizeof(Node*)); + } + + void CopyFrom(const HashMap& other) { + reserve(other.size_); + for (const auto& entry : other) + emplace(entry.first, entry.second); + } + + void StealFrom(HashMap& other) { + buckets_ = other.buckets_; + bucket_count_ = other.bucket_count_; + bucket_shift_ = other.bucket_shift_; + size_ = other.size_; + hasher_ = other.hasher_; + equal_ = other.equal_; + other.buckets_ = nullptr; + other.bucket_count_ = 0; + other.bucket_shift_ = 0; + other.size_ = 0; + } + + void Release() { + clear(); + if (buckets_) + TAllocator::Free(buckets_, bucket_count_ * sizeof(Node*)); + buckets_ = nullptr; + bucket_count_ = 0; + bucket_shift_ = 0; + } + + Node** buckets_ = nullptr; + mem_size bucket_count_ = 0; + u32 bucket_shift_ = 0; + mem_size size_ = 0; + THash hasher_{}; + TEqual equal_{}; +}; + +template , + class TEqual = Equal, + class TAllocator = DefaultAllocator> +class HashSet { + struct Empty {}; + using Map = HashMap; + + public: + using value_type = K; + + class Iterator { + public: + Iterator() = default; + explicit Iterator(typename Map::const_iterator it) : it_(it) {} + const K& operator*() const { return it_->first; } + const K* operator->() const { return &it_->first; } + Iterator& operator++() { + ++it_; + return *this; + } + Iterator operator++(int) { + Iterator before = *this; + ++it_; + return before; + } + bool operator==(const Iterator& other) const { return it_ == other.it_; } + bool operator!=(const Iterator& other) const { return it_ != other.it_; } + + private: + friend class HashSet; + typename Map::const_iterator it_; + }; + using iterator = Iterator; + using const_iterator = Iterator; + + HashSet() = default; + HashSet(std::initializer_list keys) { + reserve(keys.size()); + for (const K& key : keys) + insert(key); + } + + Iterator begin() const { return Iterator(map_.begin()); } + Iterator end() const { return Iterator(map_.end()); } + + [[nodiscard]] mem_size size() const { return map_.size(); } + [[nodiscard]] bool empty() const { return map_.empty(); } + + Pair insert(const K& key) { + auto result = map_.emplace(key); + return {Iterator(result.first), result.second}; + } + Iterator find(const K& key) const { return Iterator(map_.find(key)); } + [[nodiscard]] bool contains(const K& key) const { return map_.contains(key); } + [[nodiscard]] mem_size count(const K& key) const { return map_.count(key); } + + mem_size erase(const K& key) { return map_.erase(key); } + // Returns the element after the erased one. + Iterator erase(Iterator it) { return Iterator(map_.erase(it.it_)); } + + void clear() { map_.clear(); } + void reserve(mem_size count) { map_.reserve(count); } + + private: + Map map_; +}; + +} // namespace base diff --git a/base/containers/hash_map_test.cc b/base/containers/hash_map_test.cc new file mode 100644 index 00000000..f9d90341 --- /dev/null +++ b/base/containers/hash_map_test.cc @@ -0,0 +1,138 @@ +// Copyright (C) 2026 Vincent Hengel. +// For licensing information see LICENSE at the root of this distribution. + +#include + +#include +#include +#include + +namespace { + +TEST(HashMap, FindInsertAndOperatorBracket) { + base::HashMap map; + EXPECT_TRUE(map.find(1) == map.end()); + auto added = map.emplace(1, 10); + EXPECT_TRUE(added.second); + EXPECT_EQ(added.first->first, 1u); + EXPECT_EQ(added.first->second, 10); + EXPECT_FALSE(map.emplace(1, 99).second); + EXPECT_EQ(map[1], 10); + map[2] += 5; + EXPECT_EQ(map.at(2), 5); + EXPECT_EQ(map.size(), 2u); + EXPECT_EQ(map.count(2), 1u); + EXPECT_EQ(map.count(3), 0u); + map.insert_or_assign(2, 7); + EXPECT_EQ(map[2], 7); +} + +TEST(HashMap, ReferencesSurviveRehash) { + base::HashMap map; + u64* first = &map[0]; + *first = 1234; + for (u64 i = 1; i < 10000; i++) + map[i] = i; + EXPECT_GT(map.bucket_count(), 16u); + EXPECT_EQ(first, &map[0]); + EXPECT_EQ(*first, 1234u); + for (u64 i = 1; i < 10000; i++) + ASSERT_EQ(map.find(i)->second, i); +} + +TEST(HashMap, EraseWhileIteratingVisitsEveryElementOnce) { + base::HashMap map; + for (u64 i = 0; i < 1000; i++) + map[i] = i; + u64 seen = 0; + for (auto it = map.begin(); it != map.end();) { + seen++; + if (it->first % 2) + it = map.erase(it); + else + ++it; + } + EXPECT_EQ(seen, 1000u); + EXPECT_EQ(map.size(), 500u); + for (u64 i = 0; i < 1000; i++) + EXPECT_EQ(map.contains(i), i % 2 == 0); + EXPECT_EQ(map.erase(0), 1u); + EXPECT_EQ(map.erase(0), 0u); +} + +TEST(HashMap, IterationCoversAllEntries) { + base::HashMap map; + u64 sum = 0; + for (u64 i = 0; i < 500; i++) { + map[i * 7919] = i; + sum += i; + } + u64 total = 0; + for (auto& [key, value] : map) + total += value; + EXPECT_EQ(total, sum); + const auto& cmap = map; + u64 n = 0; + for (auto it = cmap.begin(); it != cmap.end(); ++it) + n++; + EXPECT_EQ(n, 500u); +} + +struct IdentityHash { + mem_size operator()(u64 v) const { return static_cast(v); } +}; + +TEST(HashMap, SpreadsAnIdentityHashOfAlignedKeys) { + base::HashMap map; + for (u64 i = 0; i < 4096; i++) + map[i << 16] = 1; // every key shares its low 16 bits + EXPECT_EQ(map.size(), 4096u); + for (u64 i = 0; i < 4096; i++) + ASSERT_TRUE(map.contains(i << 16)); +} + +TEST(HashMap, HoldsValuesThatCannotMove) { + base::HashMap> map; + map[1].store(5); + map.emplace(2, 7u); + EXPECT_EQ(map[1].load(), 5u); + EXPECT_EQ(map.find(2)->second.load(), 7u); +} + +TEST(HashMap, StringKeysCopyAndMove) { + base::HashMap map; + map["alpha"] = 1; + map.emplace(base::String("beta"), 2); + base::HashMap copy = map; + copy["alpha"] = 10; + EXPECT_EQ(map["alpha"], 1); + EXPECT_EQ(copy["alpha"], 10); + base::HashMap moved = base::move(copy); + EXPECT_TRUE(copy.empty()); + EXPECT_EQ(moved.size(), 2u); + moved.clear(); + EXPECT_TRUE(moved.empty()); + moved["gamma"] = 3; + EXPECT_EQ(moved.size(), 1u); +} + +TEST(HashSet, InsertFindErase) { + base::HashSet set{1, 2, 3}; + EXPECT_EQ(set.size(), 3u); + EXPECT_FALSE(set.insert(2).second); + auto added = set.insert(4); + EXPECT_TRUE(added.second); + EXPECT_EQ(*added.first, 4u); + EXPECT_TRUE(set.find(5) == set.end()); + EXPECT_EQ(set.erase(1), 1u); + EXPECT_FALSE(set.contains(1)); + u64 sum = 0; + for (u64 v : set) + sum += v; + EXPECT_EQ(sum, 2u + 3u + 4u); + for (auto it = set.begin(); it != set.end();) + it = set.erase(it); + EXPECT_TRUE(set.empty()); +} + +} // namespace diff --git a/base/containers/map.h b/base/containers/map.h index 3f27cfbc..ceec83a7 100644 --- a/base/containers/map.h +++ b/base/containers/map.h @@ -65,6 +65,16 @@ class Map { node_ = tree_->Successor(node_); return before; } + // From end() this steps to the last element. + IteratorBase& operator--() { + node_ = tree_->Predecessor(node_); + return *this; + } + IteratorBase operator--(int) { + IteratorBase before = *this; + node_ = tree_->Predecessor(node_); + return before; + } bool operator==(const IteratorBase& other) const { return node_ == other.node_; @@ -134,6 +144,20 @@ class Map { // 0 or 1, for callers written against the std::map spelling. [[nodiscard]] mem_size count(const Key& key) const { return contains(key) ? 1 : 0; } + // The first element whose key is not less than / greater than `key`. + iterator lower_bound(const Key& key) { + return iterator(tree_.LowerBound(Probe(key)), &tree_); + } + const_iterator lower_bound(const Key& key) const { + return const_iterator(tree_.LowerBound(Probe(key)), &tree_); + } + iterator upper_bound(const Key& key) { + return iterator(tree_.UpperBound(Probe(key)), &tree_); + } + const_iterator upper_bound(const Key& key) const { + return const_iterator(tree_.UpperBound(Probe(key)), &tree_); + } + bool contains(const Key& key) const { return tree_.Contains(Probe(key)); } diff --git a/base/containers/map_test.cc b/base/containers/map_test.cc index b3882bf0..55392d04 100644 --- a/base/containers/map_test.cc +++ b/base/containers/map_test.cc @@ -220,6 +220,47 @@ TEST(MapValueSemanticsTest, MoveAssignmentReplacesTheTarget) { EXPECT_TRUE(map.empty()); } +TEST(MapOrderTest, BoundsFindTheNeighbouringKeys) { + base::Map map; + for (int k : {10, 20, 30}) + map.insert(k, k * 2); + + EXPECT_EQ(map.lower_bound(20)->first, 20); + EXPECT_EQ(map.lower_bound(15)->first, 20); + EXPECT_EQ(map.upper_bound(20)->first, 30); + EXPECT_EQ(map.lower_bound(5)->first, 10); + EXPECT_TRUE(map.lower_bound(31) == map.end()); + EXPECT_TRUE(map.upper_bound(30) == map.end()); + + const base::Map& cmap = map; + EXPECT_EQ(cmap.upper_bound(10)->second, 40); +} + +TEST(MapOrderTest, IteratorsWalkBackwards) { + base::Map map; + for (int k : {3, 1, 2}) + map.insert(k, k); + + auto it = map.end(); + --it; + EXPECT_EQ(it->first, 3); + --it; + EXPECT_EQ(it->first, 2); + it--; + EXPECT_EQ(it->first, 1); + EXPECT_TRUE(it == map.begin()); + + auto hit = map.upper_bound(2); + --hit; + EXPECT_EQ(hit->first, 2); +} + +TEST(MapOrderTest, EmptyMapBoundsAreEnd) { + base::Map map; + EXPECT_TRUE(map.lower_bound(1) == map.end()); + EXPECT_TRUE(map.upper_bound(1) == map.end()); +} + TEST(SetValueSemanticsTest, CopyIsDeepAndIndependent) { base::Set set; set.insert(1); diff --git a/base/containers/set.h b/base/containers/set.h index 5d2b3cb6..706b0dc1 100644 --- a/base/containers/set.h +++ b/base/containers/set.h @@ -1,27 +1,72 @@ // Copyright (C) 2022 Vincent Hengel. // For licensing information see LICENSE at the root of this distribution. +// +// base::Set - ordered set over a red-black tree with a std::set-shaped API: +// in-order bidirectional iterators, insert/find/erase and lower/upper +// bounds. Node pointers are stable across mutation, so iterators stay valid +// for everything but the erased element. #pragma once #include #include -#include +#include #include +#include namespace base { template class Set { - private: - class IteratorImpl; + using Tree = RedBlackTree2; + using Node = typename Tree::Node; public: - using Iterator = IteratorImpl; + using value_type = T; + + // Elements are keys: iteration never hands out a mutable reference. + class Iterator { + public: + Iterator() : node_(nullptr), tree_(nullptr) {} + Iterator(Node* node, const Tree* tree) : node_(node), tree_(tree) {} + + const T& operator*() const { return node_->value; } + const T* operator->() const { return &node_->value; } + + Iterator& operator++() { + node_ = tree_->Successor(node_); + return *this; + } + Iterator operator++(int) { + Iterator before = *this; + node_ = tree_->Successor(node_); + return before; + } + // From end() this steps to the last element. + Iterator& operator--() { + node_ = tree_->Predecessor(node_); + return *this; + } + Iterator operator--(int) { + Iterator before = *this; + node_ = tree_->Predecessor(node_); + return before; + } + + bool operator==(const Iterator& other) const { return node_ == other.node_; } + bool operator!=(const Iterator& other) const { return node_ != other.node_; } + + private: + friend class Set; + Node* node_; + const Tree* tree_; + }; + using iterator = Iterator; + using const_iterator = Iterator; Set() : size_(0) {} Set(const Set& other) : tree_(other.tree_), size_(other.size_) {} - Set& operator=(const Set& other) { if (this != &other) { tree_ = other.tree_; @@ -30,11 +75,9 @@ class Set { return *this; } - // The moved-from set is left empty, not just stripped of its nodes. Set(Set&& other) noexcept : tree_(base::move(other.tree_)), size_(other.size_) { other.size_ = 0; } - Set& operator=(Set&& other) noexcept { if (this != &other) { tree_ = base::move(other.tree_); @@ -44,116 +87,77 @@ class Set { return *this; } - // Brace initialization for static tables: {value, value, ...}. Set(std::initializer_list values) : size_(0) { - for (const T& value : values) Insert(value); + for (const T& value : values) + insert(value); } - Iterator begin() const { return Iterator(tree_.root(), tree_.nil()); } - Iterator end() const { return Iterator(tree_.nil(), tree_.nil()); } + Iterator begin() const { + return Iterator(tree_.empty() ? tree_.nil() : tree_.Minimum(tree_.root()), + &tree_); + } + Iterator end() const { return Iterator(tree_.nil(), &tree_); } - bool empty() const { return tree_.empty(); } + bool empty() const { return size_ == 0; } mem_size size() const { return size_; } - void Insert(const T& value) { - if (tree_.Insert(value)) { - ++size_; - } + Pair insert(const T& value) { + bool inserted = false; + Node* node = tree_.FindOrInsert(value, &inserted); + size_ += inserted; + return {Iterator(node, &tree_), inserted}; + } + Pair insert(T&& value) { + bool inserted = false; + Node* node = tree_.FindOrInsert(base::move(value), &inserted); + size_ += inserted; + return {Iterator(node, &tree_), inserted}; } - bool Remove(const T& value) { - if (tree_.Erase(value)) { - --size_; - return true; - } - return false; + Iterator find(const T& value) const { + return Iterator(tree_.Find(value), &tree_); + } + bool contains(const T& value) const { return tree_.Contains(value); } + [[nodiscard]] mem_size count(const T& value) const { + return contains(value) ? 1 : 0; + } + + // The first element not less than / greater than `value`. + Iterator lower_bound(const T& value) const { + return Iterator(tree_.LowerBound(value), &tree_); + } + Iterator upper_bound(const T& value) const { + return Iterator(tree_.UpperBound(value), &tree_); } - bool Contains(const T& value) const { return tree_.Contains(value); } + bool erase(const T& value) { + if (!tree_.Erase(value)) + return false; + --size_; + return true; + } + // Returns the element after the erased one. + Iterator erase(Iterator it) { + Node* next = tree_.Successor(it.node_); + if (tree_.Erase(*it)) + --size_; + return Iterator(next, &tree_); + } - void Clear() { + void clear() { tree_.Clear(); size_ = 0; } - // Lowercase spellings, matching base::Map and the std::set vocabulary. - void insert(const T& value) { Insert(value); } - bool erase(const T& value) { return Remove(value); } - bool contains(const T& value) const { return Contains(value); } - [[nodiscard]] mem_size count(const T& value) const { return Contains(value) ? 1 : 0; } - void clear() { Clear(); } + // PascalCase spellings of the above. + bool Insert(const T& value) { return insert(value).second; } + bool Remove(const T& value) { return erase(value); } + bool Contains(const T& value) const { return contains(value); } + void Clear() { clear(); } private: - base::RedBlackTree2 tree_; + Tree tree_; mem_size size_; - - class IteratorImpl { - public: - using Node = typename RedBlackTree2::Node; - - IteratorImpl(Node* start_node, Node* nil_node) : nil_(nil_node) { - // set up the first iterators state - find_first(start_node); - } - - T& operator*() const { return current_->value; } - T* operator->() const { return current_->value; } - - IteratorImpl& operator++() { - // If there is a right subtree, the successor is the smallest - // element within that subtree. - if (current_->right != nil_) { - find_first(current_->right); - } - // Otherwise, the successor is the parent node from our stack. - else if (path_top_ >= 0) { - current_ = path_[path_top_--]; // Pop from stack - } - // If there's no right subtree and no parent, we've reached the end. - else { - current_ = nil_; - } - return *this; - } - - bool operator!=(const IteratorImpl& other) const { - return current_ != other.current_; - } - bool operator==(const IteratorImpl& other) const { - return current_ == other.current_; - } - - private: - // Find the left-most (smallest) node in any given subtree - void find_first(Node* node) { - path_top_ = -1; // Reset the stack for the new traversal - current_ = node; - - // Traverse down the left spine, pushing every node onto the stack. - while (current_ != nil_) { - if (path_top_ < kMaxTreeDepth - 1) { - path_[++path_top_] = current_; - } - current_ = current_->left; - } - - // The stack now holds the full path. The top of the stack is the - // left-most node, which is our first element. - if (path_top_ >= 0) { - current_ = path_[path_top_--]; // Pop to set current - } else { - // This case occurs if the initial node was nil (e.g., for end()). - current_ = nil_; - } - } - - Node* current_; - Node* nil_; - - static constexpr int kMaxTreeDepth = 128; - Node* path_[kMaxTreeDepth]; - int path_top_; - }; }; -} // namespace base \ No newline at end of file +} // namespace base diff --git a/base/containers/set_test.cc b/base/containers/set_test.cc index 10cf1db2..147d3390 100644 --- a/base/containers/set_test.cc +++ b/base/containers/set_test.cc @@ -75,4 +75,45 @@ TEST(SetTest, IteratesInSortedOrder) { EXPECT_EQ(seen, 3u); } +TEST(SetTest, IteratesEveryElementOfALargerTree) { + base::Set set; + for (i32 i = 0; i < 100; i++) + set.insert((i * 37) % 100); + + i32 expected = 0; + for (i32 v : set) + EXPECT_EQ(v, expected++); + EXPECT_EQ(expected, 100); +} + +TEST(SetTest, InsertReportsWhetherItAdded) { + base::Set set; + auto first = set.insert(4); + EXPECT_TRUE(first.second); + EXPECT_EQ(*first.first, 4); + EXPECT_FALSE(set.insert(4).second); + EXPECT_EQ(set.size(), 1u); +} + +TEST(SetTest, FindBoundsAndBackwardIteration) { + base::Set set{10, 20, 30}; + EXPECT_EQ(*set.find(20), 20); + EXPECT_TRUE(set.find(25) == set.end()); + EXPECT_EQ(*set.lower_bound(15), 20); + EXPECT_EQ(*set.upper_bound(20), 30); + auto it = set.end(); + --it; + EXPECT_EQ(*it, 30); + --it; + EXPECT_EQ(*it, 20); +} + +TEST(SetTest, EraseByIteratorReturnsTheNext) { + base::Set set{1, 2, 3}; + auto next = set.erase(set.find(2)); + EXPECT_EQ(*next, 3); + EXPECT_EQ(set.size(), 2u); + EXPECT_FALSE(set.contains(2)); +} + } // namespace diff --git a/base/containers/tree/red_black_tree_2.h b/base/containers/tree/red_black_tree_2.h index e42cc791..049fa194 100644 --- a/base/containers/tree/red_black_tree_2.h +++ b/base/containers/tree/red_black_tree_2.h @@ -291,6 +291,56 @@ class RedBlackTree2 { return node; } + Node* Maximum(Node* node) const { + while (node->right != nil_) { + node = node->right; + } + return node; + } + + // In-order predecessor; the maximum for nil(), nil() before the minimum. + Node* Predecessor(Node* node) const { + if (node == nil_) + return empty() ? nil_ : Maximum(root_); + if (node->left != nil_) { + return Maximum(node->left); + } + Node* parent = node->parent; + while (parent != nil_ && node == parent->left) { + node = parent; + parent = parent->parent; + } + return parent; + } + + // The first node not ordered before `value`, or nil(). + Node* LowerBound(const T& value) const { + Node* found = nil_; + for (Node* node = root_; node != nil_;) { + if (Comparator::less_than(node->value, value)) { + node = node->right; + } else { + found = node; + node = node->left; + } + } + return found; + } + + // The first node ordered after `value`, or nil(). + Node* UpperBound(const T& value) const { + Node* found = nil_; + for (Node* node = root_; node != nil_;) { + if (Comparator::less_than(value, node->value)) { + found = node; + node = node->left; + } else { + node = node->right; + } + } + return found; + } + // In-order successor; returns nil() past the maximum. Node pointers are // stable across Erase (deletion transplants nodes instead of moving // values), which is what makes iteration over this tree possible. diff --git a/base/containers/vector.h b/base/containers/vector.h index 7a86466a..07299163 100644 --- a/base/containers/vector.h +++ b/base/containers/vector.h @@ -345,6 +345,32 @@ class Vector { return begin() + index; } + // single element, moved in + T* insert(T* pos, T&& element) { + BASE_DCHECK(pos >= begin() && pos <= end(), "Vector::insert: Invalid position"); + const auto index = pos - begin(); + // Taken first: growing or shifting would move an element of this vector + // out from under a reference to it. + T value(base::move(element)); + + if (size() == capacity()) { + const mem_size new_cap = CalculateNewCapacity(size()); + GrowCapacity(size(), new_cap); + pos = begin() + index; + } + + MakeHoleForInsert(pos, 1); + + if (pos < end_) { + *pos = base::move(value); + } else { + ::new (static_cast(pos)) T(base::move(value)); + } + + end_++; + return begin() + index; + } + // Inserts multiple copies of an element void insert(T* pos, mem_size count, const T& element) { if (count == 0) @@ -584,6 +610,16 @@ class Vector { bool operator!=(const Vector& other) const { return !(*this == other); } + // Lexicographic, so a Vector can key an ordered container. + bool operator<(const Vector& other) const { + const mem_size n = size() < other.size() ? size() : other.size(); + for (mem_size i = 0; i < n; ++i) { + if (data_[i] < other.data_[i]) return true; + if (other.data_[i] < data_[i]) return false; + } + return size() < other.size(); + } + template void ForEach(TFunc&& func) { for (auto* it = begin(); it != end(); ++it) { diff --git a/base/containers/vector_test.cc b/base/containers/vector_test.cc index ee35dc5e..7d6c5bbd 100644 --- a/base/containers/vector_test.cc +++ b/base/containers/vector_test.cc @@ -359,6 +359,29 @@ TEST(VectorTest, AssignFromARange) { EXPECT_EQ(vec[0], 1); } +TEST(VectorTest, InsertMovesTheElementIn) { + base::Vector> outer; + outer.push_back(base::Vector{1}); + base::Vector big{7, 8, 9}; + const int* storage = big.data(); + outer.insert(outer.begin(), base::move(big)); + ASSERT_EQ(outer.size(), 2u); + EXPECT_EQ(outer[0].data(), storage); // moved, not copied + EXPECT_EQ(outer[0][2], 9); + EXPECT_EQ(outer[1][0], 1); + EXPECT_TRUE(big.empty()); +} + +TEST(VectorTest, InsertMovingAnElementOfItself) { + base::Vector> v; + v.push_back(base::Vector{1}); + v.push_back(base::Vector{2}); + v.insert(v.begin(), base::move(v[1])); + ASSERT_EQ(v.size(), 3u); + EXPECT_EQ(v[0][0], 2); + EXPECT_EQ(v[1][0], 1); +} + TEST(VectorTest, InsertAnInitializerList) { base::Vector vec = {1, 4}; diff --git a/base/math/value_bounds.h b/base/math/value_bounds.h index de7c5dcb..01b0c3f2 100644 --- a/base/math/value_bounds.h +++ b/base/math/value_bounds.h @@ -2,6 +2,8 @@ // For licensing information see LICENSE at the root of this distribution. #pragma once +#include + namespace base { template inline T Min(T a, T b) { @@ -30,4 +32,23 @@ inline T Max(const T* values, int count) { } return max_val; } + +// The smallest / largest of a non-empty list: base::Min({a, b, c}). +template +inline T Min(std::initializer_list values) { + const T* it = values.begin(); + T result = *it; + for (++it; it != values.end(); ++it) + result = (*it < result) ? *it : result; + return result; +} + +template +inline T Max(std::initializer_list values) { + const T* it = values.begin(); + T result = *it; + for (++it; it != values.end(); ++it) + result = (*it > result) ? *it : result; + return result; +} } // namespace base \ No newline at end of file diff --git a/base/memory/bit_cast.h b/base/memory/bit_cast.h new file mode 100644 index 00000000..10cc2a16 --- /dev/null +++ b/base/memory/bit_cast.h @@ -0,0 +1,14 @@ +// Copyright (C) 2026 Vincent Hengel. +// For licensing information see LICENSE at the root of this distribution. +#pragma once + +namespace base { + +// The object representation of `from`, read as a `To` of the same size. +template +constexpr To BitCast(const From& from) noexcept { + static_assert(sizeof(To) == sizeof(From), "BitCast needs equal sizes"); + return __builtin_bit_cast(To, from); +} + +} // namespace base diff --git a/base/memory/bit_cast_test.cc b/base/memory/bit_cast_test.cc new file mode 100644 index 00000000..d3014062 --- /dev/null +++ b/base/memory/bit_cast_test.cc @@ -0,0 +1,18 @@ +// Copyright (C) 2026 Vincent Hengel. +// For licensing information see LICENSE at the root of this distribution. + +#include +#include +#include + +namespace { +TEST(BitCast, FloatRoundTrips) { + EXPECT_EQ(base::BitCast(1.0f), 0x3f800000u); + EXPECT_EQ(base::BitCast(0x40490fdbu), 3.14159274f); + EXPECT_EQ(base::BitCast(base::BitCast(-2.5)), -2.5); +} + +TEST(BitCast, IsConstexpr) { + static_assert(base::BitCast(1.0f) == 0x3f800000u); +} +} // namespace diff --git a/base/memory/move.h b/base/memory/move.h index d3a72a15..e005d16e 100644 --- a/base/memory/move.h +++ b/base/memory/move.h @@ -1,95 +1,110 @@ -// Copyright (C) 2022 Vincent Hengel. -// For licensing information see LICENSE at the root of this distribution. -#pragma once - -namespace base { -template -struct remove_reference { - using type = T; -}; -template -struct remove_reference { - using type = T; -}; -template -struct remove_reference { - using type = T; -}; - -template -using remove_reference_t = typename remove_reference::type; - -template -struct remove_pointer { - using type = T; -}; - -template -struct remove_pointer { - using type = T; -}; - -template -struct remove_pointer { - using type = T; -}; - -template -using remove_pointer_t = typename remove_pointer::type; - -template -struct is_pointer { - static constexpr bool value = false; -}; - -template -struct is_pointer { - static constexpr bool value = true; -}; - -// typename remove_reference::type& -template -constexpr T&& forward(T& x) noexcept { - return static_cast(x); -} - -// typename remove_reference::type&& -template -constexpr T&& forward(T&& x) noexcept // requires(is_lvalue_reference -{ - // static_assert(!is_lvalue_reference::value, "forward T isn't lvalue - // reference"); - return static_cast(x); -} - -template -constexpr typename remove_reference::type&& move(T&& x) noexcept { - return static_cast::type&&>(x); -} - -template -[[nodiscard]] constexpr T* AddressOf(T& v) noexcept { - return __builtin_addressof(v); -} - -template -constexpr void swap(T& a, T& b) noexcept { - T tmp = static_cast(a); - a = static_cast(b); - b = static_cast(tmp); -} - -#define BASE_NOCOPYMOVE(class_name) \ - class_name(class_name&&) = delete; \ - class_name(const class_name&) = delete; \ - class_name& operator=(const class_name&) = delete; \ - class_name& operator=(class_name&&) = delete; - -#define BASE_NOMOVE(class_name) \ - class_name(class_name&&) = delete; \ - class_name& operator=(class_name&&) = delete; - -#define BASE_NOCOPY(class_name) \ - class_name(const class_name&) = delete; \ - class_name& operator=(const class_name&) = delete; -} // namespace base +// Copyright (C) 2022 Vincent Hengel. +// For licensing information see LICENSE at the root of this distribution. +#pragma once + +namespace base { +template +struct remove_reference { + using type = T; +}; +template +struct remove_reference { + using type = T; +}; +template +struct remove_reference { + using type = T; +}; + +template +using remove_reference_t = typename remove_reference::type; + +template +struct remove_const { + using type = T; +}; +template +struct remove_const { + using type = T; +}; + +template +using remove_const_t = typename remove_const::type; + +template +struct remove_pointer { + using type = T; +}; + +template +struct remove_pointer { + using type = T; +}; + +template +struct remove_pointer { + using type = T; +}; + +template +using remove_pointer_t = typename remove_pointer::type; + +template +struct is_pointer { + static constexpr bool value = false; +}; + +template +struct is_pointer { + static constexpr bool value = true; +}; + +template +inline constexpr bool is_pointer_v = is_pointer::value; + +// typename remove_reference::type& +template +constexpr T&& forward(T& x) noexcept { + return static_cast(x); +} + +// typename remove_reference::type&& +template +constexpr T&& forward(T&& x) noexcept // requires(is_lvalue_reference +{ + // static_assert(!is_lvalue_reference::value, "forward T isn't lvalue + // reference"); + return static_cast(x); +} + +template +constexpr typename remove_reference::type&& move(T&& x) noexcept { + return static_cast::type&&>(x); +} + +template +[[nodiscard]] constexpr T* AddressOf(T& v) noexcept { + return __builtin_addressof(v); +} + +template +constexpr void swap(T& a, T& b) noexcept { + T tmp = static_cast(a); + a = static_cast(b); + b = static_cast(tmp); +} + +#define BASE_NOCOPYMOVE(class_name) \ + class_name(class_name&&) = delete; \ + class_name(const class_name&) = delete; \ + class_name& operator=(const class_name&) = delete; \ + class_name& operator=(class_name&&) = delete; + +#define BASE_NOMOVE(class_name) \ + class_name(class_name&&) = delete; \ + class_name& operator=(class_name&&) = delete; + +#define BASE_NOCOPY(class_name) \ + class_name(const class_name&) = delete; \ + class_name& operator=(const class_name&) = delete; +} // namespace base diff --git a/base/memory/shared_pointer.h b/base/memory/shared_pointer.h new file mode 100644 index 00000000..dcc3f5e0 --- /dev/null +++ b/base/memory/shared_pointer.h @@ -0,0 +1,148 @@ +// Copyright (C) 2026 Vincent Hengel. +// For licensing information see LICENSE at the root of this distribution. +// +// base::SharedPointer: shared ownership with a thread-safe reference count, +// the std::shared_ptr counterpart. Constructed only through MakeShared, which +// places the object and its count in one allocation, so it never adopts a +// foreign pointer. +#pragma once + +#include +#include +#include +#include + +namespace base { + +namespace detail { +struct SharedControl { + Atomic refs{1}; + virtual void Destroy() = 0; + + protected: + ~SharedControl() = default; +}; + +template +struct SharedBlock final : SharedControl { + template + explicit SharedBlock(TArgs&&... args) : value(base::forward(args)...) {} + void Destroy() override { delete this; } + T value; +}; +} // namespace detail + +template +class SharedPointer { + public: + constexpr SharedPointer() noexcept = default; + constexpr SharedPointer(base::nullptr_t) noexcept {} + + SharedPointer(const SharedPointer& other) noexcept + : pointer_(other.pointer_), control_(other.control_) { + Retain(); + } + SharedPointer(SharedPointer&& other) noexcept + : pointer_(other.pointer_), control_(other.control_) { + other.pointer_ = nullptr; + other.control_ = nullptr; + } + + // From a pointer to a derived or less const-qualified type. + template + requires(base::ConvertibleTo) + SharedPointer(const SharedPointer& other) noexcept + : pointer_(other.pointer_), control_(other.control_) { + Retain(); + } + template + requires(base::ConvertibleTo) + SharedPointer(SharedPointer&& other) noexcept + : pointer_(other.pointer_), control_(other.control_) { + other.pointer_ = nullptr; + other.control_ = nullptr; + } + + ~SharedPointer() { Release(); } + + SharedPointer& operator=(const SharedPointer& other) noexcept { + SharedPointer(other).Swap(*this); + return *this; + } + SharedPointer& operator=(SharedPointer&& other) noexcept { + SharedPointer(base::move(other)).Swap(*this); + return *this; + } + SharedPointer& operator=(base::nullptr_t) noexcept { + Reset(); + return *this; + } + + void Reset() noexcept { + Release(); + pointer_ = nullptr; + control_ = nullptr; + } + + T* get() const noexcept { return pointer_; } + T& operator*() const noexcept { + BASE_DCHECK(pointer_); + return *pointer_; + } + T* operator->() const noexcept { + BASE_DCHECK(pointer_); + return pointer_; + } + explicit operator bool() const noexcept { return pointer_ != nullptr; } + + // Owners right now; a moment later other threads may have changed it. + mem_size use_count() const noexcept { + return control_ ? control_->refs.load(base::memory_order_relaxed) : 0; + } + + template + bool operator==(const SharedPointer& other) const noexcept { + return pointer_ == other.pointer_; + } + bool operator==(base::nullptr_t) const noexcept { return pointer_ == nullptr; } + + void Swap(SharedPointer& other) noexcept { + T* p = pointer_; + pointer_ = other.pointer_; + other.pointer_ = p; + detail::SharedControl* c = control_; + control_ = other.control_; + other.control_ = c; + } + + private: + template + friend class SharedPointer; + template + friend SharedPointer MakeShared(TArgs&&... args); + + SharedPointer(T* pointer, detail::SharedControl* control) noexcept + : pointer_(pointer), control_(control) {} + + void Retain() noexcept { + if (control_) + control_->refs.fetch_add(1, base::memory_order_relaxed); + } + void Release() noexcept { + if (control_ && + control_->refs.fetch_sub(1, base::memory_order_acq_rel) == 1) + control_->Destroy(); + } + + T* pointer_ = nullptr; + detail::SharedControl* control_ = nullptr; +}; + +template +SharedPointer MakeShared(TArgs&&... args) { + using Stored = base::remove_const_t; + auto* block = new detail::SharedBlock(base::forward(args)...); + return SharedPointer(&block->value, block); +} + +} // namespace base diff --git a/base/memory/shared_pointer_test.cc b/base/memory/shared_pointer_test.cc new file mode 100644 index 00000000..8e9a3c21 --- /dev/null +++ b/base/memory/shared_pointer_test.cc @@ -0,0 +1,105 @@ +// Copyright (C) 2026 Vincent Hengel. +// For licensing information see LICENSE at the root of this distribution. + +#include +#include +#include + +namespace { +struct Counted { + explicit Counted(int* alive, int v) : alive_(alive), value(v) { ++*alive_; } + ~Counted() { --*alive_; } + int* alive_; + int value; +}; + +struct Base { + virtual ~Base() = default; + int tag = 1; +}; +struct Derived : Base { + Derived() { tag = 2; } +}; + +TEST(SharedPointer, EmptyByDefault) { + base::SharedPointer p; + EXPECT_FALSE(p); + EXPECT_EQ(p.get(), nullptr); + EXPECT_EQ(p.use_count(), 0u); + EXPECT_TRUE(p == nullptr); +} + +TEST(SharedPointer, LastOwnerDestroys) { + int alive = 0; + { + auto a = base::MakeShared(&alive, 7); + EXPECT_EQ(alive, 1); + EXPECT_EQ(a->value, 7); + { + base::SharedPointer b = a; + EXPECT_EQ(a.use_count(), 2u); + EXPECT_EQ(b.get(), a.get()); + } + EXPECT_EQ(a.use_count(), 1u); + EXPECT_EQ(alive, 1); + } + EXPECT_EQ(alive, 0); +} + +TEST(SharedPointer, MoveLeavesSourceEmpty) { + int alive = 0; + auto a = base::MakeShared(&alive, 1); + base::SharedPointer b = base::move(a); + EXPECT_FALSE(a); + EXPECT_EQ(b.use_count(), 1u); + b.Reset(); + EXPECT_EQ(alive, 0); +} + +TEST(SharedPointer, AssignmentReleasesTheOldObject) { + int alive = 0; + auto a = base::MakeShared(&alive, 1); + auto b = base::MakeShared(&alive, 2); + EXPECT_EQ(alive, 2); + a = b; + EXPECT_EQ(alive, 1); + EXPECT_EQ(a->value, 2); + a = a; + EXPECT_EQ(a.use_count(), 2u); + b = nullptr; + EXPECT_EQ(a.use_count(), 1u); +} + +TEST(SharedPointer, ConvertsToConstAndBase) { + base::SharedPointer c = base::MakeShared(5); + EXPECT_EQ(*c, 5); + base::SharedPointer b = base::MakeShared(); + EXPECT_EQ(b->tag, 2); + auto k = base::MakeShared(9); + EXPECT_EQ(*k, 9); +} + +TEST(SharedPointer, CountsAcrossThreads) { + int alive = 0; + auto shared = base::MakeShared(&alive, 3); + constexpr int kThreads = 4, kCopies = 10000; + base::Thread* threads[kThreads]; + for (int t = 0; t < kThreads; t++) + threads[t] = new base::Thread( + "sp-test", + [shared] { + for (int i = 0; i < kCopies; i++) { + base::SharedPointer copy = shared; + (void)copy; + } + }, + true); + for (auto* t : threads) { + t->Join(); + delete t; + } + EXPECT_EQ(shared.use_count(), 1u); + shared.Reset(); + EXPECT_EQ(alive, 0); +} +} // namespace diff --git a/base/threading/condition_variable.h b/base/threading/condition_variable.h index b90485b6..86bd93e6 100644 --- a/base/threading/condition_variable.h +++ b/base/threading/condition_variable.h @@ -44,16 +44,69 @@ class BASE_EXPORT ConditionVariable { lock.lock(); } - void NotifyOne(); - void NotifyAll(); + // Waits until |pred| holds or |timeout| has passed; returns pred(), so + // false means timed out. |lock| must be held on entry and is held again on + // return. + template + bool WaitFor(Lock& lock, TimeDelta timeout, Pred pred) { + return WaitUntil(lock, TimeTicks::Now() + timeout, pred); + } + + // As WaitFor, against a deadline on the monotonic clock. + template + bool WaitUntil(Lock& lock, TimeTicks deadline, Pred pred) { + while (!pred()) { + const TimeTicks now = TimeTicks::Now(); + if (now >= deadline) + return false; + const u32 seq = seq_.load(base::memory_order_relaxed); + lock.unlock(); + WaitWhileEqualsFor(seq, deadline - now); + lock.lock(); + } + return true; + } + + // Free when nobody waits: the syscall is only made for a sleeper. + void NotifyOne() { + if (Advance()) + WakeOne(); + } + void NotifyAll() { + if (Advance()) + WakeAll(); + } private: + // Moves the sequence on; true when a waiter may be asleep on it. Both this + // and a waiter's registration are sequentially consistent, so either the + // notifier sees the waiter or the waiter's sleep sees the new sequence. + bool Advance() { + seq_.fetch_add(1, base::memory_order_seq_cst); + return waiters_.load(base::memory_order_seq_cst) != 0; + } + // Blocks while seq_ still equals |seq|. May return spuriously. - void WaitWhileEquals(u32 seq); + void WaitWhileEquals(u32 seq) { + waiters_.fetch_add(1, base::memory_order_seq_cst); + SleepWhileEquals(seq); + waiters_.fetch_sub(1, base::memory_order_relaxed); + } // As WaitWhileEquals, giving up after |timeout|. - void WaitWhileEqualsFor(u32 seq, TimeDelta timeout); + void WaitWhileEqualsFor(u32 seq, TimeDelta timeout) { + waiters_.fetch_add(1, base::memory_order_seq_cst); + SleepWhileEqualsFor(seq, timeout); + waiters_.fetch_sub(1, base::memory_order_relaxed); + } + + // The platform's address wait and wake on seq_. + void SleepWhileEquals(u32 seq); + void SleepWhileEqualsFor(u32 seq, TimeDelta timeout); + void WakeOne(); + void WakeAll(); base::Atomic seq_{0}; + base::Atomic waiters_{0}; }; } // namespace base diff --git a/base/threading/condition_variable_linux.cc b/base/threading/condition_variable_linux.cc index 8577b7cd..5f00e776 100644 --- a/base/threading/condition_variable_linux.cc +++ b/base/threading/condition_variable_linux.cc @@ -13,14 +13,14 @@ namespace base { -void ConditionVariable::WaitWhileEquals(u32 seq) { +void ConditionVariable::SleepWhileEquals(u32 seq) { int saved_errno = errno; syscall(SYS_futex, &seq_, FUTEX_WAIT | FUTEX_PRIVATE_FLAG, seq, nullptr, nullptr, 0); errno = saved_errno; } -void ConditionVariable::WaitWhileEqualsFor(u32 seq, TimeDelta timeout) { +void ConditionVariable::SleepWhileEqualsFor(u32 seq, TimeDelta timeout) { const i64 us = timeout.InMicroseconds(); if (us <= 0) return; @@ -34,16 +34,14 @@ void ConditionVariable::WaitWhileEqualsFor(u32 seq, TimeDelta timeout) { errno = saved_errno; } -void ConditionVariable::NotifyOne() { - seq_.fetch_add(1, base::memory_order_relaxed); +void ConditionVariable::WakeOne() { int saved_errno = errno; syscall(SYS_futex, &seq_, FUTEX_WAKE | FUTEX_PRIVATE_FLAG, 1, nullptr, nullptr, 0); errno = saved_errno; } -void ConditionVariable::NotifyAll() { - seq_.fetch_add(1, base::memory_order_relaxed); +void ConditionVariable::WakeAll() { int saved_errno = errno; syscall(SYS_futex, &seq_, FUTEX_WAKE | FUTEX_PRIVATE_FLAG, INT_MAX, nullptr, nullptr, 0); diff --git a/base/threading/condition_variable_mac.cc b/base/threading/condition_variable_mac.cc index 34481d5d..9fea020d 100644 --- a/base/threading/condition_variable_mac.cc +++ b/base/threading/condition_variable_mac.cc @@ -19,11 +19,11 @@ constexpr uint32_t kUlfWakeAll = 0x00000100; constexpr uint32_t kUlfNoErrno = 0x01000000; } // namespace -void ConditionVariable::WaitWhileEquals(u32 seq) { +void ConditionVariable::SleepWhileEquals(u32 seq) { __ulock_wait(kUlCompareAndWait | kUlfNoErrno, &seq_, seq, 0); } -void ConditionVariable::WaitWhileEqualsFor(u32 seq, TimeDelta timeout) { +void ConditionVariable::SleepWhileEqualsFor(u32 seq, TimeDelta timeout) { const i64 us = timeout.InMicroseconds(); if (us <= 0) return; @@ -34,13 +34,11 @@ void ConditionVariable::WaitWhileEqualsFor(u32 seq, TimeDelta timeout) { __ulock_wait(kUlCompareAndWait | kUlfNoErrno, &seq_, seq, timeout_us); } -void ConditionVariable::NotifyOne() { - seq_.fetch_add(1, base::memory_order_relaxed); +void ConditionVariable::WakeOne() { __ulock_wake(kUlCompareAndWait | kUlfNoErrno, &seq_, 0); } -void ConditionVariable::NotifyAll() { - seq_.fetch_add(1, base::memory_order_relaxed); +void ConditionVariable::WakeAll() { __ulock_wake(kUlCompareAndWait | kUlfWakeAll | kUlfNoErrno, &seq_, 0); } diff --git a/base/threading/condition_variable_test.cc b/base/threading/condition_variable_test.cc index e06e5182..85da3803 100644 --- a/base/threading/condition_variable_test.cc +++ b/base/threading/condition_variable_test.cc @@ -254,4 +254,75 @@ TEST(ConditionVariable, WaitForWakesOnNotify) { ASSERT_TRUE(notifier.Join()); } +TEST(ConditionVariable, WaitForWithPredicateTimesOut) { + base::Mutex mutex; + base::ConditionVariable cv; + Lock lock(mutex); + const base::TimeTicks start = base::TimeTicks::Now(); + EXPECT_FALSE(cv.WaitFor(lock, base::Milliseconds(20), [] { return false; })); + EXPECT_GE((base::TimeTicks::Now() - start).InMilliseconds(), 20); + EXPECT_TRUE(lock.owns_lock()); +} + +TEST(ConditionVariable, WaitForWithPredicateReturnsOnceItHolds) { + base::Mutex mutex; + base::ConditionVariable cv; + bool ready = false; + Lock lock(mutex); + base::Thread notifier( + "cv-pred", + [&] { + base::LockGuard guard(mutex); + ready = true; + cv.NotifyOne(); + }, + /*start_now=*/true); + EXPECT_TRUE(cv.WaitFor(lock, base::Seconds(60), [&] { return ready; })); + lock.unlock(); + ASSERT_TRUE(notifier.Join()); +} + +TEST(ConditionVariable, WaitUntilAPassedDeadlineChecksOnce) { + base::Mutex mutex; + base::ConditionVariable cv; + Lock lock(mutex); + int calls = 0; + EXPECT_TRUE(cv.WaitUntil(lock, base::TimeTicks::Now() - base::Seconds(1), + [&] { return ++calls > 0; })); + EXPECT_EQ(calls, 1); + EXPECT_FALSE(cv.WaitUntil(lock, base::TimeTicks::Now() - base::Seconds(1), + [] { return false; })); +} + +TEST(ConditionVariable, PingPongNeverLosesAWakeup) { + // Each side waits for the other's turn; a notify that skipped its wake while + // the other side was about to sleep would hang this test. + base::Mutex mutex; + base::ConditionVariable cv; + int turn = 0; + constexpr int kRounds = 100000; + base::Thread other( + "cv-pong", + [&] { + for (int i = 0; i < kRounds; i++) { + Lock lock(mutex); + cv.Wait(lock, [&] { return turn == 1; }); + turn = 0; + lock.unlock(); + cv.NotifyAll(); + } + }, + /*start_now=*/true); + for (int i = 0; i < kRounds; i++) { + { + base::LockGuard guard(mutex); + turn = 1; + } + cv.NotifyAll(); + Lock lock(mutex); + cv.Wait(lock, [&] { return turn == 0; }); + } + ASSERT_TRUE(other.Join()); +} + } // namespace diff --git a/base/threading/condition_variable_win.cc b/base/threading/condition_variable_win.cc index a8d4bd70..66808715 100644 --- a/base/threading/condition_variable_win.cc +++ b/base/threading/condition_variable_win.cc @@ -8,11 +8,11 @@ namespace base { -void ConditionVariable::WaitWhileEquals(u32 seq) { +void ConditionVariable::SleepWhileEquals(u32 seq) { ::WaitOnAddress(&seq_, &seq, sizeof(seq), INFINITE); } -void ConditionVariable::WaitWhileEqualsFor(u32 seq, TimeDelta timeout) { +void ConditionVariable::SleepWhileEqualsFor(u32 seq, TimeDelta timeout) { const i64 us = timeout.InMicroseconds(); if (us <= 0) return; @@ -24,13 +24,11 @@ void ConditionVariable::WaitWhileEqualsFor(u32 seq, TimeDelta timeout) { ::WaitOnAddress(&seq_, &seq, sizeof(seq), wait_ms); } -void ConditionVariable::NotifyOne() { - seq_.fetch_add(1, base::memory_order_relaxed); +void ConditionVariable::WakeOne() { ::WakeByAddressSingle(&seq_); } -void ConditionVariable::NotifyAll() { - seq_.fetch_add(1, base::memory_order_relaxed); +void ConditionVariable::WakeAll() { ::WakeByAddressAll(&seq_); } diff --git a/base/threading/recursive_mutex.h b/base/threading/recursive_mutex.h new file mode 100644 index 00000000..78984fa6 --- /dev/null +++ b/base/threading/recursive_mutex.h @@ -0,0 +1,57 @@ +// Copyright (C) 2026 Vincent Hengel. +// For licensing information see LICENSE at the root of this distribution. +#pragma once + +#include +#include +#include + +namespace base { + +// A base::Mutex its owning thread may lock again; every lock needs its unlock. +class RecursiveMutex { + public: + void lock() { + const mem_size self = ThreadTag(); + if (owner_.load(base::memory_order_relaxed) == self) { + depth_++; + return; + } + mutex_.lock(); + owner_.store(self, base::memory_order_relaxed); + depth_ = 1; + } + + bool try_lock() { + const mem_size self = ThreadTag(); + if (owner_.load(base::memory_order_relaxed) == self) { + depth_++; + return true; + } + if (!mutex_.try_lock()) + return false; + owner_.store(self, base::memory_order_relaxed); + depth_ = 1; + return true; + } + + void unlock() { + if (--depth_) + return; + owner_.store(0, base::memory_order_relaxed); + mutex_.unlock(); + } + + private: + // Unique per live thread, without asking the OS. + static mem_size ThreadTag() { + static thread_local char tag; + return reinterpret_cast(&tag); + } + + Mutex mutex_; + Atomic owner_{0}; + mem_size depth_ = 0; +}; + +} // namespace base diff --git a/base/threading/recursive_mutex_test.cc b/base/threading/recursive_mutex_test.cc new file mode 100644 index 00000000..368dace5 --- /dev/null +++ b/base/threading/recursive_mutex_test.cc @@ -0,0 +1,60 @@ +// Copyright (C) 2026 Vincent Hengel. +// For licensing information see LICENSE at the root of this distribution. + +#include +#include +#include +#include + +namespace { +TEST(RecursiveMutex, OwnerMayRelock) { + base::RecursiveMutex m; + m.lock(); + m.lock(); + EXPECT_TRUE(m.try_lock()); + m.unlock(); + m.unlock(); + m.unlock(); + EXPECT_TRUE(m.try_lock()); + m.unlock(); +} + +TEST(RecursiveMutex, ExcludesOtherThreads) { + base::RecursiveMutex m; + m.lock(); + m.lock(); + bool other_got_it = true; + base::Thread other("rm-test", [&] { other_got_it = m.try_lock(); }, true); + other.Join(); + EXPECT_FALSE(other_got_it); + m.unlock(); + m.unlock(); + base::Thread after("rm-test", [&] { + other_got_it = m.try_lock(); + if (other_got_it) + m.unlock(); + }, true); + after.Join(); + EXPECT_TRUE(other_got_it); +} + +TEST(RecursiveMutex, CountsStayConsistentUnderContention) { + base::RecursiveMutex m; + int counter = 0; + constexpr int kThreads = 4, kIters = 20000; + base::Thread* threads[kThreads]; + for (int t = 0; t < kThreads; t++) + threads[t] = new base::Thread("rm-test", [&] { + for (int i = 0; i < kIters; i++) { + base::LockGuard outer(m); + base::LockGuard inner(m); + counter++; + } + }, true); + for (auto* t : threads) { + t->Join(); + delete t; + } + EXPECT_EQ(counter, kThreads * kIters); +} +} // namespace diff --git a/base/threading/thread.h b/base/threading/thread.h index 33ce5eb8..ce406fc6 100644 --- a/base/threading/thread.h +++ b/base/threading/thread.h @@ -36,6 +36,7 @@ class BASE_EXPORT Thread { // since both are same size anyway, we can use the same type bool good() const { return handle_data_.handle_ != nullptr; } + Handle handle() const { return handle_data_; } void SetName(const base::StringRef name); @@ -60,6 +61,15 @@ BASE_EXPORT u32 GetCurrentThreadIndex(); BASE_EXPORT Thread::Handle GetCurrentThreadHandle(); BASE_EXPORT bool SetThreadName(Thread::Handle, const char* name); + +// Whether |handle| names the calling thread. +BASE_EXPORT bool IsCurrentThread(Thread::Handle handle); + +// Runs |functor| on a new thread nobody joins, like a detached std::thread. +// The thread owns the functor and frees it when it returns. False when no +// thread could be created. +BASE_EXPORT bool SpawnDetachedThread(const base::StringRef name, + base::Function functor); inline bool SetCurrentThreadName(const char* name) { return SetThreadName(GetCurrentThreadHandle(), name); } diff --git a/base/threading/thread_posix.cc b/base/threading/thread_posix.cc index 2fe1f0e8..de72c26c 100644 --- a/base/threading/thread_posix.cc +++ b/base/threading/thread_posix.cc @@ -159,4 +159,40 @@ u32 GetProcessorCount() { Thread::Handle GetCurrentThreadHandle() { return {.pthread_ = PthreadToHandle(::pthread_self())}; } + +bool IsCurrentThread(Thread::Handle handle) { + return ::pthread_equal(::pthread_self(), HandleToPthread(handle.pthread_)) != 0; +} + +namespace { +struct DetachedStart { + base::String name; + base::Function functor; +}; + +void* DetachedThreadFunc(void* user_param) { + auto* start = static_cast(user_param); + base::SetThreadName(base::GetCurrentThreadHandle(), start->name.c_str()); + start->functor(); + delete start; + return nullptr; +} +} // namespace + +bool SpawnDetachedThread(const base::StringRef name, + base::Function functor) { + auto* start = new DetachedStart{base::String(name.data(), name.length()), + base::move(functor)}; + pthread_attr_t attributes; + pthread_attr_init(&attributes); + pthread_attr_setdetachstate(&attributes, PTHREAD_CREATE_DETACHED); + pthread_t handle{}; + const int ec = ::pthread_create(&handle, &attributes, DetachedThreadFunc, start); + pthread_attr_destroy(&attributes); + if (ec != 0) { + delete start; + return false; + } + return true; +} } // namespace base diff --git a/base/threading/thread_test.cc b/base/threading/thread_test.cc index c6d2adfa..1c422ee9 100644 --- a/base/threading/thread_test.cc +++ b/base/threading/thread_test.cc @@ -131,4 +131,46 @@ TEST(Thread, ProcessorCountIsNonZero) { EXPECT_GE(base::GetProcessorCount(), 1u); } +TEST(Thread, IsCurrentThreadTellsThreadsApart) { + base::Atomic on_itself{0}; + base::Thread* self = nullptr; + base::Atomic started{false}; + base::Thread thread( + "is-current", + [&] { + while (!started.load()) + base::YieldCurrentThread(); + on_itself.store(base::IsCurrentThread(self->handle()) ? 1 : 2); + }, + /*start_now=*/false); + self = &thread; + ASSERT_TRUE(thread.Start(base::Thread::Priority::kNormal)); + EXPECT_FALSE(base::IsCurrentThread(thread.handle())); + started.store(true); + ASSERT_TRUE(thread.Join()); + EXPECT_EQ(on_itself.load(), 1); + EXPECT_TRUE(base::IsCurrentThread(base::GetCurrentThreadHandle())); +} + +TEST(Thread, DetachedThreadRunsAndFreesItsFunctor) { + struct Tracked { + base::Atomic* alive; + explicit Tracked(base::Atomic* a) : alive(a) { alive->fetch_add(1); } + Tracked(const Tracked& o) : alive(o.alive) { alive->fetch_add(1); } + ~Tracked() { alive->fetch_sub(1); } + }; + base::Atomic alive{0}; + base::Atomic ran{0}; + { + Tracked tracked(&alive); + ASSERT_TRUE(base::SpawnDetachedThread( + "detached", [tracked, &ran] { ran.store(1); })); + } + // Nobody can join it: poll for the run and for the functor's release. + for (int i = 0; i < 5000 && (ran.load() == 0 || alive.load() != 0); i++) + base::SleepForMicroseconds(1000); + EXPECT_EQ(ran.load(), 1); + EXPECT_EQ(alive.load(), 0); +} + } // namespace diff --git a/base/threading/thread_win.cc b/base/threading/thread_win.cc index 9c582dc1..683f28a8 100644 --- a/base/threading/thread_win.cc +++ b/base/threading/thread_win.cc @@ -135,4 +135,37 @@ u32 GetCurrentThreadIndex() { Thread::Handle GetCurrentThreadHandle() { return {.handle_ = ::GetCurrentThread()}; } + +bool IsCurrentThread(Thread::Handle handle) { + return handle.handle_ && ::GetThreadId(handle.handle_) == ::GetCurrentThreadId(); +} + +namespace { +struct DetachedStart { + base::String name; + base::Function functor; +}; + +DWORD WINAPI DetachedThreadFunc(LPVOID user_param) { + auto* start = static_cast(user_param); + base::SetThreadName(base::GetCurrentThreadHandle(), start->name.c_str()); + start->functor(); + delete start; + return 0; +} +} // namespace + +bool SpawnDetachedThread(const base::StringRef name, + base::Function functor) { + auto* start = new DetachedStart{base::String(name.data(), name.length()), + base::move(functor)}; + HANDLE handle = ::CreateThread(nullptr, 0, DetachedThreadFunc, start, 0, nullptr); + if (!handle) { + delete start; + return false; + } + // Nobody waits for it: drop our reference and let it run. + ::CloseHandle(handle); + return true; +} } // namespace base \ No newline at end of file