From 8a3250d2f4287c2f66c4afd7679f9b10f789e764 Mon Sep 17 00:00:00 2001 From: Aaron Green Date: Thu, 19 Sep 2024 17:55:27 +0000 Subject: [PATCH] pw_containers: Add IntrusiveMap and IntrusiveMultiMap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This CL adds intrusive, ordered maps backed by an AA tree implementation. These can be used for associative dictionaries or for sorted lists, among other uses. Change-Id: I81194bcdbff70a807f52547e99a04649c5b61635 Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/216828 Lint: Lint 🤖 Reviewed-by: Wyatt Hepler Commit-Queue: Aaron Green --- docs/BUILD.gn | 2 + pw_containers/BUILD.bazel | 66 +- pw_containers/BUILD.gn | 53 +- pw_containers/CMakeLists.txt | 61 + pw_containers/aa_tree.cc | 59 + pw_containers/aa_tree_item.cc | 210 ++++ pw_containers/docs.rst | 12 + pw_containers/intrusive_map_test.cc | 893 ++++++++++++++ pw_containers/intrusive_multimap_test.cc | 1061 +++++++++++++++++ .../public/pw_containers/internal/aa_tree.h | 390 ++++++ .../pw_containers/internal/aa_tree_item.h | 154 +++ .../pw_containers/internal/aa_tree_iterator.h | 127 ++ .../pw_containers/internal/intrusive_list.h | 4 +- .../public/pw_containers/intrusive_map.h | 304 +++++ .../public/pw_containers/intrusive_multimap.h | 278 +++++ 15 files changed, 3664 insertions(+), 10 deletions(-) create mode 100644 pw_containers/aa_tree.cc create mode 100644 pw_containers/aa_tree_item.cc create mode 100644 pw_containers/intrusive_map_test.cc create mode 100644 pw_containers/intrusive_multimap_test.cc create mode 100644 pw_containers/public/pw_containers/internal/aa_tree.h create mode 100644 pw_containers/public/pw_containers/internal/aa_tree_item.h create mode 100644 pw_containers/public/pw_containers/internal/aa_tree_iterator.h create mode 100644 pw_containers/public/pw_containers/intrusive_map.h create mode 100644 pw_containers/public/pw_containers/intrusive_multimap.h diff --git a/docs/BUILD.gn b/docs/BUILD.gn index a266df654a..aaf770df1b 100644 --- a/docs/BUILD.gn +++ b/docs/BUILD.gn @@ -239,6 +239,8 @@ _doxygen_input_files = [ # keep-sorted: start "$dir_pw_containers/public/pw_containers/inline_var_len_entry_queue.h", "$dir_pw_containers/public/pw_containers/intrusive_forward_list.h", "$dir_pw_containers/public/pw_containers/intrusive_list.h", + "$dir_pw_containers/public/pw_containers/intrusive_map.h", + "$dir_pw_containers/public/pw_containers/intrusive_multimap.h", "$dir_pw_crypto/public/pw_crypto/ecdsa.h", "$dir_pw_crypto/public/pw_crypto/sha256.h", "$dir_pw_digital_io/public/pw_digital_io/digital_io.h", diff --git a/pw_containers/BUILD.bazel b/pw_containers/BUILD.bazel index 6b7ef0c22d..42fc740be1 100644 --- a/pw_containers/BUILD.bazel +++ b/pw_containers/BUILD.bazel @@ -38,13 +38,14 @@ label_flag( # Libraries cc_library( + # This group is deprecated. Prefer to depend on individual features rather + # than on this collection. name = "pw_containers", deps = [ ":algorithm", ":flat_map", ":inline_deque", ":inline_queue", - ":intrusive_forward_list", ":intrusive_list", ":vector", ], @@ -107,6 +108,39 @@ cc_library( deps = [":intrusive_forward_list"], ) +cc_library( + name = "intrusive_map_common", + srcs = [ + "aa_tree.cc", + "aa_tree_item.cc", + ], + hdrs = [ + "public/pw_containers/internal/aa_tree.h", + "public/pw_containers/internal/aa_tree_item.h", + "public/pw_containers/internal/aa_tree_iterator.h", + ], + includes = ["public"], + deps = [ + ":intrusive", + "//pw_assert", + "//pw_bytes:packed_ptr", + ], +) + +cc_library( + name = "intrusive_map", + hdrs = ["public/pw_containers/intrusive_map.h"], + includes = ["public"], + deps = [":intrusive_map_common"], +) + +cc_library( + name = "intrusive_multimap", + hdrs = ["public/pw_containers/intrusive_multimap.h"], + includes = ["public"], + deps = [":intrusive_map_common"], +) + cc_library( name = "iterator", hdrs = ["public/pw_containers/iterator.h"], @@ -324,9 +358,7 @@ pw_cc_test( pw_cc_test( name = "intrusive_forward_list_test", - srcs = [ - "intrusive_forward_list_test.cc", - ], + srcs = ["intrusive_forward_list_test.cc"], deps = [ ":intrusive_forward_list", ":vector", @@ -337,9 +369,7 @@ pw_cc_test( pw_cc_test( name = "intrusive_list_test", - srcs = [ - "intrusive_list_test.cc", - ], + srcs = ["intrusive_list_test.cc"], deps = [ ":intrusive_list", ":vector", @@ -347,3 +377,25 @@ pw_cc_test( "//pw_unit_test", ], ) + +pw_cc_test( + name = "intrusive_map_test", + srcs = ["intrusive_map_test.cc"], + deps = [ + ":intrusive_map", + ":intrusive_multimap", + "//pw_compilation_testing:negative_compilation_testing", + "//pw_span", + ], +) + +pw_cc_test( + name = "intrusive_multimap_test", + srcs = ["intrusive_multimap_test.cc"], + deps = [ + ":intrusive_map", + ":intrusive_multimap", + "//pw_compilation_testing:negative_compilation_testing", + "//pw_span", + ], +) diff --git a/pw_containers/BUILD.gn b/pw_containers/BUILD.gn index c3fd0cb2ea..c5a97efe10 100644 --- a/pw_containers/BUILD.gn +++ b/pw_containers/BUILD.gn @@ -49,7 +49,6 @@ group("pw_containers") { ":flat_map", ":inline_deque", ":inline_queue", - ":intrusive_forward_list", ":intrusive_list", ":vector", ] @@ -185,6 +184,36 @@ pw_source_set("legacy_intrusive_list") { public_deps = [ ":intrusive_forward_list" ] } +pw_source_set("intrusive_map_common") { + public_configs = [ ":public_include_path" ] + public = [ + "public/pw_containers/internal/aa_tree.h", + "public/pw_containers/internal/aa_tree_item.h", + "public/pw_containers/internal/aa_tree_iterator.h", + ] + public_deps = [ + ":intrusive", + "$dir_pw_bytes:packed_ptr", + dir_pw_assert, + ] + sources = [ + "aa_tree.cc", + "aa_tree_item.cc", + ] +} + +pw_source_set("intrusive_map") { + public_configs = [ ":public_include_path" ] + public = [ "public/pw_containers/intrusive_map.h" ] + public_deps = [ ":intrusive_map_common" ] +} + +pw_source_set("intrusive_multimap") { + public_configs = [ ":public_include_path" ] + public = [ "public/pw_containers/intrusive_multimap.h" ] + public_deps = [ ":intrusive_map_common" ] +} + pw_test_group("tests") { tests = [ ":algorithm_test", @@ -194,6 +223,8 @@ pw_test_group("tests") { ":inline_queue_test", ":intrusive_forward_list_test", ":intrusive_list_test", + ":intrusive_map_test", + ":intrusive_multimap_test", ":raw_storage_test", ":to_array_test", ":inline_var_len_entry_queue_test", @@ -346,6 +377,26 @@ pw_test("intrusive_list_test") { configs = [ "$dir_pw_build:conversion_warnings" ] } +pw_test("intrusive_map_test") { + sources = [ "intrusive_map_test.cc" ] + deps = [ + ":intrusive_map", + ":intrusive_multimap", + dir_pw_span, + ] + negative_compilation_tests = true +} + +pw_test("intrusive_multimap_test") { + sources = [ "intrusive_multimap_test.cc" ] + deps = [ + ":intrusive_map", + ":intrusive_multimap", + dir_pw_span, + ] + negative_compilation_tests = true +} + pw_doc_group("docs") { sources = [ "docs.rst" ] report_deps = [ ":intrusive_list_size_report" ] diff --git a/pw_containers/CMakeLists.txt b/pw_containers/CMakeLists.txt index 77b3df1e38..20370248e8 100644 --- a/pw_containers/CMakeLists.txt +++ b/pw_containers/CMakeLists.txt @@ -36,6 +36,7 @@ pw_add_library(pw_containers INTERFACE pw_containers.inline_deque pw_containers.inline_queue pw_containers.intrusive_list + pw_containers.intrusive_map pw_containers.vector ) @@ -198,6 +199,40 @@ pw_add_library(pw_containers.legacy_intrusive_list INTERFACE pw_containers.intrusive_forward_list ) +pw_add_library(pw_containers.intrusive_map_common STATIC + HEADERS + public/pw_containers/internal/aa_tree.h + public/pw_containers/internal/aa_tree_item.h + public/pw_containers/internal/aa_tree_iterator.h + PUBLIC_INCLUDES + public + PUBLIC_DEPS + pw_assert + pw_bytes.packed_ptr + pw_containers.intrusive + SOURCES + aa_tree.cc + aa_tree_item.cc +) + +pw_add_library(pw_containers.intrusive_map INTERFACE + HEADERS + public/pw_containers/intrusive_map.h + PUBLIC_INCLUDES + public + PUBLIC_DEPS + pw_containers.intrusive_map_common +) + +pw_add_library(pw_containers.intrusive_multimap INTERFACE + HEADERS + public/pw_containers/intrusive_multimap.h + PUBLIC_INCLUDES + public + PUBLIC_DEPS + pw_containers.intrusive_map_common +) + pw_add_test(pw_containers.algorithm_test SOURCES algorithm_test.cc @@ -342,3 +377,29 @@ pw_add_test(pw_containers.intrusive_list_test modules pw_containers ) + +pw_add_test(pw_containers.intrusive_map_test + SOURCES + intrusive_map_test.cc + PRIVATE_DEPS + pw_compilation_testing._pigweed_only_negative_compilation + pw_containers.intrusive_map + pw_containers.intrusive_multimap + pw_span + GROUPS + modules + pw_containers +) + +pw_add_test(pw_containers.intrusive_multimap_test + SOURCES + intrusive_multimap_test.cc + PRIVATE_DEPS + pw_compilation_testing._pigweed_only_negative_compilation + pw_containers.intrusive_map + pw_containers.intrusive_multimap + pw_span + GROUPS + modules + pw_containers +) diff --git a/pw_containers/aa_tree.cc b/pw_containers/aa_tree.cc new file mode 100644 index 0000000000..3f16357202 --- /dev/null +++ b/pw_containers/aa_tree.cc @@ -0,0 +1,59 @@ +// Copyright 2024 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +#include "pw_containers/internal/aa_tree.h" + +namespace pw::containers::internal { + +void GenericAATree::SetRoot(AATreeItem* item) { + if (item != nullptr) { + item->parent_.set(nullptr); + } + root_ = item; +} + +size_t GenericAATree::size() const { + return empty() ? 0 : root_->GetTreeSize(); +} + +void GenericAATree::clear() { + if (root_ != nullptr) { + root_->Clear(); + SetRoot(nullptr); + } +} + +GenericAATree::iterator GenericAATree::erase(AATreeItem& item) { + if (item.GetRoot() != root_) { + return iterator(&root_); + } + AATreeItem* next = item.GetSuccessor(); + SetRoot(item.Unmap()); + return iterator(&root_, next); +} + +GenericAATree::iterator GenericAATree::erase(AATreeItem& first, + AATreeItem& last) { + iterator iter(&root_, &first); + while (&(*iter) != &last) { + iter = erase(*iter); + } + return iter; +} + +void GenericAATree::swap(GenericAATree& other) { + std::swap(root_, other.root_); +} + +} // namespace pw::containers::internal diff --git a/pw_containers/aa_tree_item.cc b/pw_containers/aa_tree_item.cc new file mode 100644 index 0000000000..681f742cbb --- /dev/null +++ b/pw_containers/aa_tree_item.cc @@ -0,0 +1,210 @@ +// Copyright 2024 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +#include "pw_containers/internal/aa_tree_item.h" + +namespace pw::containers::internal { + +uint8_t AATreeItem::GetLevel() const { + return parent_.packed_value() | (left_.packed_value() << 2) | + (right_.packed_value() << 4); +} + +void AATreeItem::SetLevel(uint8_t level) { + parent_.set_packed_value(level & 3); + left_.set_packed_value((level >> 2) & 3); + right_.set_packed_value((level >> 4) & 3); +} + +bool AATreeItem::IsMapped() const { + return GetLevel() != 0 || parent_.get() != nullptr || + left_.get() != nullptr || right_.get() != nullptr; +} + +size_t AATreeItem::GetTreeSize() { + return 1 + (left_.get() == nullptr ? 0 : left_->GetTreeSize()) + + (right_.get() == nullptr ? 0 : right_->GetTreeSize()); +} + +AATreeItem* AATreeItem::GetRoot() { + AATreeItem* node = this; + while (node->parent_.get() != nullptr) { + node = node->parent_.get(); + } + return node; +} + +AATreeItem* AATreeItem::GetLeftmost() { + return left_.get() == nullptr ? this : left_->GetLeftmost(); +} + +AATreeItem* AATreeItem::GetRightmost() { + return right_.get() == nullptr ? this : right_->GetRightmost(); +} + +AATreeItem* AATreeItem::GetPredecessor() { + if (left_.get() != nullptr) { + return left_->GetRightmost(); + } + const AATreeItem* current = this; + AATreeItem* ancestor = parent_.get(); + while (ancestor != nullptr && ancestor->left_.get() == current) { + current = ancestor; + ancestor = ancestor->parent_.get(); + } + return ancestor; +} + +AATreeItem* AATreeItem::GetSuccessor() { + if (right_.get() != nullptr) { + return right_->GetLeftmost(); + } + const AATreeItem* current = this; + AATreeItem* ancestor = parent_.get(); + while (ancestor != nullptr && ancestor->right_.get() == current) { + current = ancestor; + ancestor = ancestor->parent_.get(); + } + return ancestor; +} + +AATreeItem* AATreeItem::Unmap() { + AATreeItem* node; + if (left_.get() == nullptr && right_.get() == nullptr) { + // Leaf node. + node = nullptr; + + } else if (left_.get() == nullptr) { + // Replace the node with the next one in value. + node = GetSuccessor(); + right_->parent_.set(nullptr); + node->SetRight(node->Unmap()); + + } else { + // Replace the node with the previous one in value. + node = GetPredecessor(); + left_->parent_.set(nullptr); + node->SetLeft(node->Unmap()); + node->SetRight(right_.get()); + } + if (parent_.get() != nullptr) { + parent_->Replace(this, node); + } else if (node == nullptr) { + // Removing the only node from the tree. + Reset(); + return nullptr; + } + node = node == nullptr ? GetRoot() : node->Rebalance(); + Reset(); + return node; +} + +void AATreeItem::SetLeft(AATreeItem* left) { + if (left != nullptr) { + left->parent_.set(this); + } + left_.set(left); +} + +void AATreeItem::SetRight(AATreeItem* right) { + if (right != nullptr) { + right->parent_.set(this); + } + right_.set(right); +} + +void AATreeItem::Replace(AATreeItem* old_child, AATreeItem* new_child) { + if (left_.get() == old_child) { + SetLeft(new_child); + } else if (right_.get() == old_child) { + SetRight(new_child); + } +} + +AATreeItem* AATreeItem::Skew() { + if (left_.get() == nullptr || GetLevel() != left_->GetLevel()) { + return this; + } + AATreeItem* skewed = left_.get(); + SetLeft(skewed->right_.get()); + skewed->parent_.set(parent_.get()); + skewed->SetRight(this); + return skewed; +} + +AATreeItem* AATreeItem::Split() { + if (right_.get() == nullptr || right_->right_.get() == nullptr || + GetLevel() != right_->right_->GetLevel()) { + return this; + } + AATreeItem* split = right_.get(); + SetRight(split->left_.get()); + split->parent_.set(parent_.get()); + split->SetLeft(this); + split->SetLevel(split->GetLevel() + 1); + return split; +} + +AATreeItem* AATreeItem::Rebalance() { + AATreeItem* node = this; + while (true) { + uint8_t left_level = + node->left_.get() == nullptr ? 0 : node->left_->GetLevel(); + uint8_t right_level = + node->right_.get() == nullptr ? 0 : node->right_->GetLevel(); + uint8_t new_level = std::min(left_level, right_level) + 1; + if (new_level < node->GetLevel()) { + node->SetLevel(new_level); + if (new_level < right_level) { + node->right_->SetLevel(new_level); + } + } + node = node->Skew(); + if (node->right_.get() != nullptr) { + node->SetRight(node->right_->Skew()); + if (node->right_->right_.get() != nullptr) { + node->right_->SetRight(node->right_->right_->Skew()); + } + } + node = node->Split(); + if (node->right_.get() != nullptr) { + node->SetRight(node->right_->Split()); + } + if (node->parent_.get() == nullptr) { + return node; + } + node = node->parent_.get(); + } +} + +void AATreeItem::Clear() { + if (parent_.get() != nullptr) { + parent_->Replace(this, nullptr); + } + if (left_.get() != nullptr) { + left_->Clear(); + } + if (right_.get() != nullptr) { + right_->Clear(); + } + Reset(); +} + +void AATreeItem::Reset() { + parent_ = PackedPtr(); + left_ = PackedPtr(); + right_ = PackedPtr(); +} + +} // namespace pw::containers::internal diff --git a/pw_containers/docs.rst b/pw_containers/docs.rst index ba15a457d0..51136c36da 100644 --- a/pw_containers/docs.rst +++ b/pw_containers/docs.rst @@ -308,6 +308,18 @@ a ``FlatMap`` with two items. Pair{-3, 'b'}, }; +---------------- +pw::IntrusiveMap +---------------- +.. doxygenclass:: pw::IntrusiveMap + :members: + +--------------------- +pw::IntrusiveMultiMap +--------------------- +.. doxygenclass:: pw::IntrusiveMultiMap + :members: + ---------------------------- pw::containers::FilteredView ---------------------------- diff --git a/pw_containers/intrusive_map_test.cc b/pw_containers/intrusive_map_test.cc new file mode 100644 index 0000000000..c56fca6b71 --- /dev/null +++ b/pw_containers/intrusive_map_test.cc @@ -0,0 +1,893 @@ +// Copyright 2024 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +#include "pw_containers/intrusive_map.h" + +#include "pw_compilation_testing/negative_compilation.h" +#include "pw_containers/intrusive_multimap.h" +#include "pw_span/span.h" +#include "pw_unit_test/framework.h" + +namespace { + +// Base item. +class BaseItem { + public: + BaseItem(size_t key, const char* name) : key_(key), name_(name) {} + + constexpr const size_t& key() const { return key_; } + constexpr const char* name() const { return name_; } + void set_name(const char* name) { name_ = name; } + + private: + size_t key_; + const char* name_; +}; + +// A basic item that can be used in a map. +struct TestItem : public ::pw::IntrusiveMap::Item, + public BaseItem { + TestItem(size_t key, const char* name) : BaseItem(key, name) {} +}; + +// Test fixture. +class IntrusiveMapTest : public ::testing::Test { + protected: + using IntrusiveMap = ::pw::IntrusiveMap; + static constexpr size_t kNumItems = 5; + + void SetUp() override { map_.insert(items_.begin(), items_.end()); } + + void TearDown() override { map_.clear(); } + + std::array items_ = {{ + {30, "a"}, + {50, "b"}, + {20, "c"}, + {40, "d"}, + {10, "e"}, + }}; + + IntrusiveMap map_; +}; + +// Unit tests. + +TEST_F(IntrusiveMapTest, Construct_Default) { + IntrusiveMap map; + EXPECT_TRUE(map.empty()); + EXPECT_EQ(map.begin(), map.end()); + EXPECT_EQ(map.rbegin(), map.rend()); + EXPECT_EQ(map.size(), 0U); + EXPECT_EQ(map.lower_bound(0), map.end()); + EXPECT_EQ(map.upper_bound(0), map.end()); +} + +TEST_F(IntrusiveMapTest, Construct_ObjectIterators) { + map_.clear(); + IntrusiveMap map(items_.begin(), items_.end()); + EXPECT_FALSE(map.empty()); + EXPECT_EQ(map.size(), items_.size()); + map.clear(); +} + +TEST_F(IntrusiveMapTest, Construct_ObjectIterators_Empty) { + IntrusiveMap map(items_.end(), items_.end()); + EXPECT_TRUE(map.empty()); + EXPECT_EQ(map.size(), 0U); +} + +TEST_F(IntrusiveMapTest, Construct_PointerIterators) { + std::array ptrs = {&items_[0], &items_[1], &items_[2]}; + map_.clear(); + IntrusiveMap map(ptrs.begin(), ptrs.end()); + EXPECT_FALSE(map.empty()); + EXPECT_EQ(map.size(), 3U); + map.clear(); +} + +TEST_F(IntrusiveMapTest, Construct_PointerIterators_Empty) { + std::array ptrs; + IntrusiveMap map(ptrs.begin(), ptrs.end()); + EXPECT_TRUE(map.empty()); + EXPECT_EQ(map.size(), 0U); + map.clear(); +} + +TEST_F(IntrusiveMapTest, Construct_InitializerList) { + map_.clear(); + IntrusiveMap map({&items_[0], &items_[2], &items_[4]}); + EXPECT_FALSE(map.empty()); + EXPECT_EQ(map.size(), 3U); + map.clear(); +} + +TEST_F(IntrusiveMapTest, Construct_InitializerList_Empty) { + IntrusiveMap map({}); + EXPECT_TRUE(map.empty()); + EXPECT_EQ(map.size(), 0U); +} + +TEST_F(IntrusiveMapTest, Construct_CustomCompare) { + map_.clear(); + using Compare = std::greater; + using CustomMapType = pw::IntrusiveMap; + + CustomMapType map(items_.begin(), items_.end()); + EXPECT_EQ(map.size(), items_.size()); + + auto iter = map.begin(); + EXPECT_EQ((iter++)->key(), 50U); + EXPECT_EQ((iter++)->key(), 40U); + EXPECT_EQ((iter++)->key(), 30U); + EXPECT_EQ((iter++)->key(), 20U); + EXPECT_EQ((iter++)->key(), 10U); + map.clear(); +} + +// Functor for comparing names. +struct StrCmp { + bool operator()(const char* lhs, const char* rhs) { + return std::strcmp(lhs, rhs) < 0; + } +}; + +// This functor is similar to `internal::GetKey`, but uses +// the first character of the name instead. +struct GetName { + const char* operator()( + const ::pw::containers::internal::AATreeItem& item) const { + return static_cast(item).name(); + } +}; + +TEST_F(IntrusiveMapTest, Construct_CustomGetKey) { + map_.clear(); + using CustomMapType = + pw::IntrusiveMap; + CustomMapType map(items_.begin(), items_.end()); + auto iter = map.begin(); + EXPECT_STREQ((iter++)->name(), "a"); + EXPECT_STREQ((iter++)->name(), "b"); + EXPECT_STREQ((iter++)->name(), "c"); + EXPECT_STREQ((iter++)->name(), "d"); + EXPECT_STREQ((iter++)->name(), "e"); + EXPECT_EQ(iter, map.end()); + map.clear(); +} + +// A struct that is not a map item. +struct NotAnItem : public BaseItem { + NotAnItem(size_t key, const char* name) : BaseItem(key, name) {} +}; + +#if PW_NC_TEST(IncompatibleItem) +PW_NC_EXPECT( + "IntrusiveMap items must be derived from IntrusiveMap::Item"); + +struct BadItem : public ::pw::IntrusiveMap::Item {}; + +[[maybe_unused]] ::pw::IntrusiveMap bad_map1; + +#elif PW_NC_TEST(DoesNotInheritFromItem) +PW_NC_EXPECT( + "IntrusiveMap items must be derived from IntrusiveMap::Item"); + +[[maybe_unused]] ::pw::IntrusiveMap bad_map2; + +#endif // PW_NC_TEST + +// Element access + +TEST_F(IntrusiveMapTest, At) { + const IntrusiveMap& map = map_; + for (const auto& item : items_) { + EXPECT_EQ(&(map.at(item.key())), &item); + } +} + +// Iterators + +TEST_F(IntrusiveMapTest, Iterator) { + const IntrusiveMap& map = map_; + auto iter = map.begin(); + size_t key = 10; + for (size_t i = 0; i < kNumItems; ++i) { + auto& item = *iter++; + EXPECT_EQ(item.key(), key); + key += 10; + } + EXPECT_EQ(key, 60U); + EXPECT_EQ(iter, map.end()); + EXPECT_EQ(iter, map.cend()); + for (size_t i = 0; i < kNumItems; ++i) { + key -= 10; + EXPECT_EQ((--iter)->key(), key); + } + EXPECT_EQ(key, 10U); + EXPECT_EQ(iter, map.begin()); + EXPECT_EQ(iter, map.cbegin()); +} + +TEST_F(IntrusiveMapTest, ReverseIterator) { + const IntrusiveMap& map = map_; + auto iter = map.rbegin(); + size_t key = 50; + for (size_t i = 0; i < kNumItems; ++i) { + auto& item = *iter++; + EXPECT_EQ(item.key(), key); + key -= 10; + } + EXPECT_EQ(key, 0U); + EXPECT_EQ(iter, map.rend()); + EXPECT_EQ(iter, map.crend()); + for (size_t i = 0; i < kNumItems; ++i) { + key += 10; + EXPECT_EQ((--iter)->key(), key); + } + EXPECT_EQ(key, 50U); + EXPECT_EQ(iter, map.rbegin()); + EXPECT_EQ(iter, map.crbegin()); +} + +TEST_F(IntrusiveMapTest, ConstIterator_CompareNonConst) { + EXPECT_EQ(map_.end(), map_.cend()); +} + +// A map item that is distinct from TestItem +struct OtherItem : public ::pw::IntrusiveMap::Item, + public BaseItem { + OtherItem(size_t key, const char* name) : BaseItem(key, name) {} +}; + +TEST_F(IntrusiveMapTest, ConstIterator_CompareNonConst_CompilationFails) { + ::pw::IntrusiveMap map; +#if PW_NC_TEST(CannotCompareIncompatibleIteratorsEqual) + PW_NC_EXPECT("map_\.end\(\) == map\.end\(\)"); + static_cast(map_.end() == map.end()); +#elif PW_NC_TEST(CannotCompareIncompatibleIteratorsInequal) + PW_NC_EXPECT("map_\.end\(\) != map\.end\(\)"); + static_cast(map_.end() != map.end()); +#endif // PW_NC_TEST +} + +#if PW_NC_TEST(CannotModifyThroughConstIterator) +PW_NC_EXPECT("function is not marked const|discards qualifiers"); + +TEST_F(IntrusiveMapTest, ConstIterator_Modify) { + const IntrusiveMap& map = map_; + auto iter = map.begin(); + iter->set_name("nope"); +} + +#endif // PW_NC_TEST + +// Capacity + +TEST_F(IntrusiveMapTest, IsEmpty) { + const IntrusiveMap& map = map_; + EXPECT_FALSE(map.empty()); + map_.clear(); + EXPECT_TRUE(map.empty()); +} + +TEST_F(IntrusiveMapTest, GetSize) { + const IntrusiveMap& map = map_; + EXPECT_EQ(map.size(), kNumItems); + map_.clear(); + EXPECT_EQ(map.size(), 0U); +} + +TEST_F(IntrusiveMapTest, GetMaxSize) { + const IntrusiveMap& map = map_; + EXPECT_EQ(map.max_size(), size_t(std::numeric_limits::max())); +} + +// Modifiers + +// This functions allows tests to use `std::is_sorted` without specializing +// `std::less`. Since `std::less` is the default value for the +// `Compare` template parameter, leaving it untouched avoids accidentally +// masking type-handling errors. +constexpr bool LessThan(const TestItem& lhs, const TestItem& rhs) { + return lhs.key() < rhs.key(); +} + +TEST_F(IntrusiveMapTest, Insert) { + map_.clear(); + bool sorted = true; + size_t prev_key = 0; + for (auto& item : items_) { + sorted &= prev_key < item.key(); + + // Use the "hinted" version of insert. + map_.insert(map_.end(), item); + prev_key = item.key(); + } + EXPECT_FALSE(sorted); + + EXPECT_EQ(map_.size(), kNumItems); + EXPECT_TRUE(std::is_sorted(map_.begin(), map_.end(), LessThan)); +} + +TEST_F(IntrusiveMapTest, Insert_Duplicate) { + TestItem item1(60, "1"); + TestItem item2(60, "2"); + + auto result = map_.insert(item1); + EXPECT_STREQ(result.first->name(), "1"); + EXPECT_TRUE(result.second); + + result = map_.insert(item2); + EXPECT_STREQ(result.first->name(), "1"); + EXPECT_FALSE(result.second); + + EXPECT_EQ(map_.size(), kNumItems + 1); + EXPECT_TRUE(std::is_sorted(map_.begin(), map_.end(), LessThan)); + + // Explicitly clear the map before item 1 goes out of scope. + map_.clear(); +} + +TEST_F(IntrusiveMapTest, Insert_ObjectIterators) { + map_.clear(); + map_.insert(items_.begin(), items_.end()); + EXPECT_EQ(map_.size(), kNumItems); + EXPECT_TRUE(std::is_sorted(map_.begin(), map_.end(), LessThan)); +} + +TEST_F(IntrusiveMapTest, Insert_ObjectIterators_Empty) { + map_.insert(items_.end(), items_.end()); + EXPECT_EQ(map_.size(), kNumItems); + EXPECT_TRUE(std::is_sorted(map_.begin(), map_.end(), LessThan)); +} + +TEST_F(IntrusiveMapTest, Insert_ObjectIterators_WithDuplicates) { + std::array items = {{ + {50, "B"}, + {40, "D"}, + {60, "F"}, + }}; + + map_.insert(items.begin(), items.end()); + EXPECT_EQ(map_.size(), kNumItems + 1); + EXPECT_TRUE(std::is_sorted(map_.begin(), map_.end(), LessThan)); + EXPECT_STREQ(map_.at(40).name(), "d"); + EXPECT_STREQ(map_.at(50).name(), "b"); + EXPECT_STREQ(map_.at(60).name(), "F"); + + // Explicitly clear the map before items goes out of scope. + map_.clear(); +} + +TEST_F(IntrusiveMapTest, Insert_PointerIterators) { + map_.clear(); + std::array ptrs = {&items_[0], &items_[1], &items_[2]}; + + map_.insert(ptrs.begin(), ptrs.end()); + EXPECT_EQ(map_.size(), 3U); + EXPECT_TRUE(std::is_sorted(map_.begin(), map_.end(), LessThan)); +} + +TEST_F(IntrusiveMapTest, Insert_PointerIterators_Empty) { + std::array ptrs; + + map_.insert(ptrs.begin(), ptrs.end()); + EXPECT_EQ(map_.size(), kNumItems); + EXPECT_TRUE(std::is_sorted(map_.begin(), map_.end(), LessThan)); +} + +TEST_F(IntrusiveMapTest, Insert_PointerIterators_WithDuplicates) { + TestItem item1(50, "B"); + TestItem item2(40, "D"); + TestItem item3(60, "F"); + std::array ptrs = {&item1, &item2, &item3}; + + map_.insert(ptrs.begin(), ptrs.end()); + EXPECT_EQ(map_.size(), kNumItems + 1); + EXPECT_TRUE(std::is_sorted(map_.begin(), map_.end(), LessThan)); + EXPECT_STREQ(map_.at(40).name(), "d"); + EXPECT_STREQ(map_.at(50).name(), "b"); + EXPECT_STREQ(map_.at(60).name(), "F"); + + // Explicitly clear the map before items goes out of scope. + map_.clear(); +} + +TEST_F(IntrusiveMapTest, Insert_InitializerList) { + map_.clear(); + map_.insert({&items_[0], &items_[2], &items_[4]}); + EXPECT_EQ(map_.size(), 3U); + EXPECT_TRUE(std::is_sorted(map_.begin(), map_.end(), LessThan)); +} + +TEST_F(IntrusiveMapTest, Insert_InitializerList_Empty) { + map_.insert({}); + EXPECT_EQ(map_.size(), kNumItems); + EXPECT_TRUE(std::is_sorted(map_.begin(), map_.end(), LessThan)); +} + +TEST_F(IntrusiveMapTest, Insert_InitializerList_WithDuplicates) { + TestItem item1(50, "B"); + TestItem item2(40, "D"); + TestItem item3(60, "F"); + + map_.insert({&item1, &item2, &item3}); + EXPECT_EQ(map_.size(), kNumItems + 1); + EXPECT_TRUE(std::is_sorted(map_.begin(), map_.end(), LessThan)); + EXPECT_STREQ(map_.at(40).name(), "d"); + EXPECT_STREQ(map_.at(50).name(), "b"); + EXPECT_STREQ(map_.at(60).name(), "F"); + + // Explicitly clear the map before items goes out of scope. + map_.clear(); +} + +// An item derived from TestItem. +struct DerivedItem : public TestItem { + DerivedItem(size_t n, const char* name) : TestItem(n * 10, name) {} +}; + +TEST_F(IntrusiveMapTest, Insert_DerivedItems) { + DerivedItem item1(6, "f"); + map_.insert(item1); + + DerivedItem item2(7, "g"); + map_.insert(item2); + + EXPECT_EQ(map_.size(), kNumItems + 2); + EXPECT_TRUE(std::is_sorted(map_.begin(), map_.end(), LessThan)); + + // Explicitly clear the map before items goes out of scope. + map_.clear(); +} + +TEST_F(IntrusiveMapTest, Insert_DerivedItems_CompilationFails) { + ::pw::IntrusiveMap derived_from_compatible_item_type; + + DerivedItem item1(6, "f"); + derived_from_compatible_item_type.insert(item1); + + EXPECT_EQ(derived_from_compatible_item_type.size(), 1U); + +#if PW_NC_TEST(CannotAddBaseClassToDerivedClassMap) + PW_NC_EXPECT("derived_from_compatible_item_type\.insert\(item2\)"); + + TestItem item2(70, "g"); + derived_from_compatible_item_type.insert(item2); +#endif + derived_from_compatible_item_type.clear(); +} + +TEST_F(IntrusiveMapTest, Erase_OneItem) { + EXPECT_EQ(map_.size(), kNumItems); + EXPECT_EQ(map_.erase(items_[2].key()), 1U); + EXPECT_EQ(map_.size(), kNumItems - 1); + + auto iter = map_.find(items_[2].key()); + EXPECT_EQ(iter, map_.end()); +} + +TEST_F(IntrusiveMapTest, Erase_OnlyItem) { + map_.clear(); + map_.insert(items_[0]); + EXPECT_EQ(map_.size(), 1U); + + EXPECT_EQ(map_.erase(items_[0].key()), 1U); + EXPECT_EQ(map_.size(), 0U); +} + +TEST_F(IntrusiveMapTest, Erase_AllOnebyOne) { + auto iter = map_.begin(); + for (size_t n = kNumItems; n != 0; --n) { + ASSERT_NE(iter, map_.end()); + iter = map_.erase(iter); + } + EXPECT_EQ(iter, map_.end()); + EXPECT_EQ(map_.size(), 0U); +} + +TEST_F(IntrusiveMapTest, Erase_Range) { + auto first = map_.begin(); + auto last = map_.end(); + ++first; + --last; + auto iter = map_.erase(first, last); + EXPECT_EQ(map_.size(), 2U); + EXPECT_TRUE(std::is_sorted(map_.begin(), map_.end(), LessThan)); + EXPECT_EQ(iter->key(), 50U); +} + +TEST_F(IntrusiveMapTest, Erase_MissingItem) { EXPECT_EQ(map_.erase(100), 0U); } + +TEST_F(IntrusiveMapTest, Erase_Reinsert) { + EXPECT_EQ(map_.size(), items_.size()); + + EXPECT_EQ(map_.erase(items_[0].key()), 1U); + EXPECT_EQ(map_.find(items_[0].key()), map_.end()); + + EXPECT_EQ(map_.erase(items_[2].key()), 1U); + EXPECT_EQ(map_.find(items_[2].key()), map_.end()); + + EXPECT_EQ(map_.erase(items_[4].key()), 1U); + EXPECT_EQ(map_.find(items_[4].key()), map_.end()); + + EXPECT_EQ(map_.size(), items_.size() - 3); + + map_.insert(items_[4]); + auto iter = map_.find(items_[4].key()); + EXPECT_NE(iter, map_.end()); + + map_.insert(items_[0]); + iter = map_.find(items_[0].key()); + EXPECT_NE(iter, map_.end()); + + map_.insert(items_[2]); + iter = map_.find(items_[2].key()); + EXPECT_NE(iter, map_.end()); + + EXPECT_EQ(map_.size(), items_.size()); +} + +TEST_F(IntrusiveMapTest, Swap) { + std::array items = {{ + {50, "B"}, + {40, "D"}, + {60, "F"}, + }}; + IntrusiveMap map(items.begin(), items.end()); + + map_.swap(map); + EXPECT_EQ(map.size(), kNumItems); + EXPECT_TRUE(std::is_sorted(map.begin(), map.end(), LessThan)); + EXPECT_EQ(map.at(30).name(), "a"); + EXPECT_EQ(map.at(50).name(), "b"); + EXPECT_EQ(map.at(20).name(), "c"); + EXPECT_EQ(map.at(40).name(), "d"); + EXPECT_EQ(map.at(10).name(), "e"); + map.clear(); + + EXPECT_EQ(map_.size(), 3U); + EXPECT_TRUE(std::is_sorted(map_.begin(), map_.end(), LessThan)); + EXPECT_STREQ(map_.at(50).name(), "B"); + EXPECT_STREQ(map_.at(40).name(), "D"); + EXPECT_STREQ(map_.at(60).name(), "F"); + + // Explicitly clear the map before items goes out of scope. + map_.clear(); +} + +TEST_F(IntrusiveMapTest, Swap_Empty) { + IntrusiveMap map; + + map_.swap(map); + EXPECT_EQ(map.size(), kNumItems); + EXPECT_TRUE(std::is_sorted(map.begin(), map.end(), LessThan)); + EXPECT_EQ(map.at(30).name(), "a"); + EXPECT_EQ(map.at(50).name(), "b"); + EXPECT_EQ(map.at(20).name(), "c"); + EXPECT_EQ(map.at(40).name(), "d"); + EXPECT_EQ(map.at(10).name(), "e"); + map.clear(); + + EXPECT_EQ(map_.size(), 0U); +} + +TEST_F(IntrusiveMapTest, Merge) { + std::array items = {{ + {15, "f"}, + {45, "g"}, + {65, "h"}, + }}; + IntrusiveMap map(items.begin(), items.end()); + + map_.merge(map); + EXPECT_TRUE(map.empty()); + EXPECT_EQ(map_.size(), kNumItems + 3); + EXPECT_TRUE(std::is_sorted(map_.begin(), map_.end(), LessThan)); + EXPECT_STREQ(map_.at(30).name(), "a"); + EXPECT_STREQ(map_.at(50).name(), "b"); + EXPECT_STREQ(map_.at(20).name(), "c"); + EXPECT_STREQ(map_.at(40).name(), "d"); + EXPECT_STREQ(map_.at(10).name(), "e"); + EXPECT_STREQ(map_.at(15).name(), "f"); + EXPECT_STREQ(map_.at(45).name(), "g"); + EXPECT_STREQ(map_.at(65).name(), "h"); + + // Explicitly clear the map before items goes out of scope. + map_.clear(); +} + +TEST_F(IntrusiveMapTest, Merge_Empty) { + IntrusiveMap map; + + map_.merge(map); + EXPECT_EQ(map_.size(), kNumItems); + EXPECT_TRUE(std::is_sorted(map_.begin(), map_.end(), LessThan)); + + map.merge(map_); + EXPECT_TRUE(map_.empty()); + EXPECT_EQ(map.size(), kNumItems); + EXPECT_TRUE(std::is_sorted(map.begin(), map.end(), LessThan)); + + map.clear(); +} + +TEST_F(IntrusiveMapTest, Merge_WithDuplicates) { + std::array items = {{ + {50, "B"}, + {40, "D"}, + {60, "F"}, + }}; + IntrusiveMap map(items.begin(), items.end()); + + map_.merge(map); + EXPECT_TRUE(map.empty()); + EXPECT_EQ(map_.size(), kNumItems + 1); + EXPECT_TRUE(std::is_sorted(map_.begin(), map_.end(), LessThan)); + EXPECT_STREQ(map_.at(30).name(), "a"); + EXPECT_STREQ(map_.at(50).name(), "b"); + EXPECT_STREQ(map_.at(20).name(), "c"); + EXPECT_STREQ(map_.at(40).name(), "d"); + EXPECT_STREQ(map_.at(10).name(), "e"); + EXPECT_STREQ(map_.at(60).name(), "F"); + + // Explicitly clear the map before items goes out of scope. + map_.clear(); +} + +// A struct for a multimap instead of a map. +struct MultiMapItem + : public ::pw::IntrusiveMultiMap::Item, + public BaseItem { + MultiMapItem(size_t key, const char* name) : BaseItem(key, name) {} +}; + +TEST_F(IntrusiveMapTest, Merge_MultiMap) { + std::array items = {{ + {50, "B"}, + {40, "D"}, + {60, "F"}, + }}; + ::pw::IntrusiveMultiMap multimap(items.begin(), + items.end()); + + map_.merge(multimap); + EXPECT_TRUE(multimap.empty()); + EXPECT_EQ(map_.size(), kNumItems + 1); + EXPECT_TRUE(std::is_sorted(map_.begin(), map_.end(), LessThan)); + EXPECT_STREQ(map_.at(30).name(), "a"); + EXPECT_STREQ(map_.at(50).name(), "b"); + EXPECT_STREQ(map_.at(20).name(), "c"); + EXPECT_STREQ(map_.at(40).name(), "d"); + EXPECT_STREQ(map_.at(10).name(), "e"); + EXPECT_STREQ(map_.at(60).name(), "F"); + + // Explicitly clear the map before items goes out of scope. + map_.clear(); +} + +TEST_F(IntrusiveMapTest, Count) { + const IntrusiveMap& map = map_; + EXPECT_EQ(map.count(10), 1U); + EXPECT_EQ(map.count(20), 1U); + EXPECT_EQ(map.count(30), 1U); + EXPECT_EQ(map.count(40), 1U); + EXPECT_EQ(map.count(50), 1U); +} + +TEST_F(IntrusiveMapTest, Count_NoSuchKey) { + const IntrusiveMap& map = map_; + EXPECT_EQ(map.count(60), 0U); +} + +TEST_F(IntrusiveMapTest, Find) { + const IntrusiveMap& map = map_; + size_t key = 0; + for (size_t i = 0; i < kNumItems; ++i) { + key += 10; + auto iter = map.find(key); + ASSERT_NE(iter, map.end()); + EXPECT_EQ(iter->key(), key); + } +} + +TEST_F(IntrusiveMapTest, Find_NoSuchKey) { + const IntrusiveMap& map = map_; + auto iter = map.find(45); + EXPECT_EQ(iter, map.end()); +} + +TEST_F(IntrusiveMapTest, LowerBound) { + const IntrusiveMap& map = map_; + auto iter = map.lower_bound(10); + ASSERT_NE(iter, map.end()); + EXPECT_STREQ(iter->name(), "e"); + + iter = map.lower_bound(20); + ASSERT_NE(iter, map.end()); + EXPECT_STREQ(iter->name(), "c"); + + iter = map.lower_bound(30); + ASSERT_NE(iter, map.end()); + EXPECT_STREQ(iter->name(), "a"); + + iter = map.lower_bound(40); + ASSERT_NE(iter, map.end()); + EXPECT_STREQ(iter->name(), "d"); + + iter = map.lower_bound(50); + ASSERT_NE(iter, map.end()); + EXPECT_STREQ(iter->name(), "b"); +} + +TEST_F(IntrusiveMapTest, LowerBound_NoExactKey) { + const IntrusiveMap& map = map_; + auto iter = map.lower_bound(5); + ASSERT_NE(iter, map.end()); + EXPECT_STREQ(iter->name(), "e"); + + iter = map.lower_bound(15); + ASSERT_NE(iter, map.end()); + EXPECT_STREQ(iter->name(), "c"); + + iter = map.lower_bound(25); + ASSERT_NE(iter, map.end()); + EXPECT_STREQ(iter->name(), "a"); + + iter = map.lower_bound(35); + ASSERT_NE(iter, map.end()); + EXPECT_STREQ(iter->name(), "d"); + + iter = map.lower_bound(45); + ASSERT_NE(iter, map.end()); + EXPECT_STREQ(iter->name(), "b"); +} + +TEST_F(IntrusiveMapTest, LowerBound_OutOfRange) { + const IntrusiveMap& map = map_; + EXPECT_EQ(map.lower_bound(55), map.end()); +} + +TEST_F(IntrusiveMapTest, UpperBound) { + const IntrusiveMap& map = map_; + auto iter = map.upper_bound(10); + ASSERT_NE(iter, map.end()); + EXPECT_STREQ(iter->name(), "c"); + + iter = map.upper_bound(20); + ASSERT_NE(iter, map.end()); + EXPECT_STREQ(iter->name(), "a"); + + iter = map.upper_bound(30); + ASSERT_NE(iter, map.end()); + EXPECT_STREQ(iter->name(), "d"); + + iter = map.upper_bound(40); + ASSERT_NE(iter, map.end()); + EXPECT_STREQ(iter->name(), "b"); + + EXPECT_EQ(map.upper_bound(50), map.end()); +} + +TEST_F(IntrusiveMapTest, UpperBound_NoExactKey) { + const IntrusiveMap& map = map_; + auto iter = map.upper_bound(5); + ASSERT_NE(iter, map.end()); + EXPECT_STREQ(iter->name(), "e"); + + iter = map.upper_bound(15); + ASSERT_NE(iter, map.end()); + EXPECT_STREQ(iter->name(), "c"); + + iter = map.upper_bound(25); + ASSERT_NE(iter, map.end()); + EXPECT_STREQ(iter->name(), "a"); + + iter = map.upper_bound(35); + ASSERT_NE(iter, map.end()); + EXPECT_STREQ(iter->name(), "d"); + + iter = map.upper_bound(45); + ASSERT_NE(iter, map.end()); + EXPECT_STREQ(iter->name(), "b"); +} + +TEST_F(IntrusiveMapTest, UpperBound_OutOfRange) { + const IntrusiveMap& map = map_; + EXPECT_EQ(map.upper_bound(55), map.end()); +} + +TEST_F(IntrusiveMapTest, EqualRange) { + const IntrusiveMap& map = map_; + + auto pair = map.equal_range(10); + IntrusiveMap::const_iterator lower = pair.first; + IntrusiveMap::const_iterator upper = pair.second; + ASSERT_NE(lower, map.end()); + EXPECT_STREQ(lower->name(), "e"); + ASSERT_NE(upper, map.end()); + EXPECT_STREQ(upper->name(), "c"); + + std::tie(lower, upper) = map.equal_range(20); + ASSERT_NE(lower, map.end()); + EXPECT_STREQ(lower->name(), "c"); + ASSERT_NE(upper, map.end()); + EXPECT_STREQ(upper->name(), "a"); + + std::tie(lower, upper) = map.equal_range(30); + ASSERT_NE(lower, map.end()); + EXPECT_STREQ(lower->name(), "a"); + ASSERT_NE(upper, map.end()); + EXPECT_STREQ(upper->name(), "d"); + + std::tie(lower, upper) = map.equal_range(40); + ASSERT_NE(lower, map.end()); + EXPECT_STREQ(lower->name(), "d"); + ASSERT_NE(upper, map.end()); + EXPECT_STREQ(upper->name(), "b"); + + std::tie(lower, upper) = map.equal_range(50); + ASSERT_NE(lower, map.end()); + EXPECT_STREQ(lower->name(), "b"); + EXPECT_EQ(upper, map.end()); +} + +TEST_F(IntrusiveMapTest, EqualRange_NoExactKey) { + const IntrusiveMap& map = map_; + + auto pair = map.equal_range(5); + IntrusiveMap::const_iterator lower = pair.first; + IntrusiveMap::const_iterator upper = pair.second; + ASSERT_NE(lower, map.end()); + EXPECT_STREQ(lower->name(), "e"); + ASSERT_NE(upper, map.end()); + EXPECT_STREQ(upper->name(), "e"); + + std::tie(lower, upper) = map.equal_range(15); + ASSERT_NE(lower, map.end()); + EXPECT_STREQ(lower->name(), "c"); + ASSERT_NE(upper, map.end()); + EXPECT_STREQ(upper->name(), "c"); + + std::tie(lower, upper) = map.equal_range(25); + ASSERT_NE(lower, map.end()); + EXPECT_STREQ(lower->name(), "a"); + ASSERT_NE(upper, map.end()); + EXPECT_STREQ(upper->name(), "a"); + + std::tie(lower, upper) = map.equal_range(35); + ASSERT_NE(lower, map.end()); + EXPECT_STREQ(lower->name(), "d"); + ASSERT_NE(upper, map.end()); + EXPECT_STREQ(upper->name(), "d"); + + std::tie(lower, upper) = map.equal_range(45); + ASSERT_NE(lower, map.end()); + EXPECT_STREQ(lower->name(), "b"); + ASSERT_NE(upper, map.end()); + EXPECT_STREQ(upper->name(), "b"); +} + +TEST_F(IntrusiveMapTest, EqualRange_OutOfRange) { + const IntrusiveMap& map = map_; + + auto pair = map.equal_range(55); + IntrusiveMap::const_iterator lower = pair.first; + IntrusiveMap::const_iterator upper = pair.second; + EXPECT_EQ(lower, map.end()); + EXPECT_EQ(upper, map.end()); +} + +} // namespace diff --git a/pw_containers/intrusive_multimap_test.cc b/pw_containers/intrusive_multimap_test.cc new file mode 100644 index 0000000000..47ce5f016f --- /dev/null +++ b/pw_containers/intrusive_multimap_test.cc @@ -0,0 +1,1061 @@ +// Copyright 2024 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +#include "pw_containers/intrusive_multimap.h" + +#include "pw_compilation_testing/negative_compilation.h" +#include "pw_containers/intrusive_map.h" +#include "pw_span/span.h" +#include "pw_unit_test/framework.h" + +namespace { + +// Base item. +class BaseItem { + public: + BaseItem(size_t key, const char* name) : key_(key), name_(name) {} + + constexpr const size_t& key() const { return key_; } + constexpr const char* name() const { return name_; } + void set_name(const char* name) { name_ = name; } + + private: + size_t key_; + const char* name_; +}; + +// A basic item that can be used in a map. +struct TestItem : public ::pw::IntrusiveMultiMap::Item, + public BaseItem { + TestItem(size_t key, const char* name) : BaseItem(key, name) {} +}; + +// Test fixture. +class IntrusiveMultiMapTest : public ::testing::Test { + protected: + using IntrusiveMultiMap = ::pw::IntrusiveMultiMap; + static constexpr size_t kNumItems = 5; + + void SetUp() override { multimap_.insert(items_.begin(), items_.end()); } + + void TearDown() override { multimap_.clear(); } + + std::array items_ = {{ + {30, "a"}, + {50, "b"}, + {20, "c"}, + {40, "d"}, + {10, "e"}, + }}; + + IntrusiveMultiMap multimap_; +}; + +// Unit tests. + +TEST_F(IntrusiveMultiMapTest, Construct_Default) { + IntrusiveMultiMap multimap; + EXPECT_TRUE(multimap.empty()); + EXPECT_EQ(multimap.begin(), multimap.end()); + EXPECT_EQ(multimap.rbegin(), multimap.rend()); + EXPECT_EQ(multimap.size(), 0U); + EXPECT_EQ(multimap.lower_bound(0), multimap.end()); + EXPECT_EQ(multimap.upper_bound(0), multimap.end()); +} + +TEST_F(IntrusiveMultiMapTest, Construct_ObjectIterators) { + multimap_.clear(); + IntrusiveMultiMap multimap(items_.begin(), items_.end()); + EXPECT_FALSE(multimap.empty()); + EXPECT_EQ(multimap.size(), items_.size()); + multimap.clear(); +} + +TEST_F(IntrusiveMultiMapTest, Construct_ObjectIterators_Empty) { + IntrusiveMultiMap multimap(items_.end(), items_.end()); + EXPECT_TRUE(multimap.empty()); + EXPECT_EQ(multimap.size(), 0U); +} + +TEST_F(IntrusiveMultiMapTest, Construct_PointerIterators) { + std::array ptrs = {&items_[0], &items_[1], &items_[2]}; + multimap_.clear(); + IntrusiveMultiMap multimap(ptrs.begin(), ptrs.end()); + EXPECT_FALSE(multimap.empty()); + EXPECT_EQ(multimap.size(), 3U); + multimap.clear(); +} + +TEST_F(IntrusiveMultiMapTest, Construct_PointerIterators_Empty) { + std::array ptrs; + IntrusiveMultiMap multimap(ptrs.begin(), ptrs.end()); + EXPECT_TRUE(multimap.empty()); + EXPECT_EQ(multimap.size(), 0U); + multimap.clear(); +} + +TEST_F(IntrusiveMultiMapTest, Construct_InitializerList) { + multimap_.clear(); + IntrusiveMultiMap multimap({&items_[0], &items_[2], &items_[4]}); + EXPECT_FALSE(multimap.empty()); + EXPECT_EQ(multimap.size(), 3U); + multimap.clear(); +} + +TEST_F(IntrusiveMultiMapTest, Construct_InitializerList_Empty) { + IntrusiveMultiMap multimap({}); + EXPECT_TRUE(multimap.empty()); + EXPECT_EQ(multimap.size(), 0U); +} + +TEST_F(IntrusiveMultiMapTest, Construct_CustomCompare) { + multimap_.clear(); + using Compare = std::greater; + using CustomMapType = pw::IntrusiveMultiMap; + CustomMapType multimap(items_.begin(), items_.end()); + auto iter = multimap.begin(); + EXPECT_EQ((iter++)->key(), 50U); + EXPECT_EQ((iter++)->key(), 40U); + EXPECT_EQ((iter++)->key(), 30U); + EXPECT_EQ((iter++)->key(), 20U); + EXPECT_EQ((iter++)->key(), 10U); + EXPECT_EQ(iter, multimap.end()); + multimap.clear(); +} + +// Functor for comparing names. +struct StrCmp { + bool operator()(const char* lhs, const char* rhs) { + return std::strcmp(lhs, rhs) < 0; + } +}; + +// This functor is similar to `internal::GetKey`, but uses +// the first character of the name instead. +struct GetName { + const char* operator()( + const ::pw::containers::internal::AATreeItem& item) const { + return static_cast(item).name(); + } +}; + +TEST_F(IntrusiveMultiMapTest, Construct_CustomGetKey) { + multimap_.clear(); + using CustomMapType = + pw::IntrusiveMultiMap; + CustomMapType multimap(items_.begin(), items_.end()); + auto iter = multimap.begin(); + EXPECT_STREQ((iter++)->name(), "a"); + EXPECT_STREQ((iter++)->name(), "b"); + EXPECT_STREQ((iter++)->name(), "c"); + EXPECT_STREQ((iter++)->name(), "d"); + EXPECT_STREQ((iter++)->name(), "e"); + EXPECT_EQ(iter, multimap.end()); + multimap.clear(); +} + +// A struct that is not a multimap item. +struct NotAnItem : public BaseItem { + NotAnItem(size_t key, const char* name) : BaseItem(key, name) {} +}; + +#if PW_NC_TEST(IncompatibleItem) +PW_NC_EXPECT( + "IntrusiveMultiMap items must be derived from IntrusiveMultiMap::Item"); + +class BadItem : public ::pw::IntrusiveMultiMap::Item {}; + +[[maybe_unused]] ::pw::IntrusiveMultiMap bad_multimap1; + +#elif PW_NC_TEST(DoesNotInheritFromItem) +PW_NC_EXPECT( + "IntrusiveMultiMap items must be derived from IntrusiveMultiMap::Item"); + +[[maybe_unused]] ::pw::IntrusiveMultiMap bad_multimap2; + +#endif // PW_NC_TEST + +// Iterators + +TEST_F(IntrusiveMultiMapTest, Iterator) { + const IntrusiveMultiMap& multimap = multimap_; + auto iter = multimap.begin(); + size_t key = 10; + for (size_t i = 0; i < kNumItems; ++i) { + auto& item = *iter++; + EXPECT_EQ(item.key(), key); + key += 10; + } + EXPECT_EQ(key, 60U); + EXPECT_EQ(iter, multimap.end()); + EXPECT_EQ(iter, multimap.cend()); + for (size_t i = 0; i < kNumItems; ++i) { + key -= 10; + EXPECT_EQ((--iter)->key(), key); + } + EXPECT_EQ(key, 10U); + EXPECT_EQ(iter, multimap.begin()); + EXPECT_EQ(iter, multimap.cbegin()); +} + +TEST_F(IntrusiveMultiMapTest, ReverseIterator) { + const IntrusiveMultiMap& multimap = multimap_; + auto iter = multimap.rbegin(); + size_t key = 50; + for (size_t i = 0; i < kNumItems; ++i) { + auto& item = *iter++; + EXPECT_EQ(item.key(), key); + key -= 10; + } + EXPECT_EQ(key, 0U); + EXPECT_EQ(iter, multimap.rend()); + EXPECT_EQ(iter, multimap.crend()); + for (size_t i = 0; i < kNumItems; ++i) { + key += 10; + EXPECT_EQ((--iter)->key(), key); + } + EXPECT_EQ(key, 50U); + EXPECT_EQ(iter, multimap.rbegin()); + EXPECT_EQ(iter, multimap.crbegin()); +} + +TEST_F(IntrusiveMultiMapTest, ConstIterator_CompareNonConst) { + EXPECT_EQ(multimap_.end(), multimap_.cend()); +} + +// A multimap item that is distinct from TestItem +struct OtherItem : public ::pw::IntrusiveMultiMap::Item, + public BaseItem { + OtherItem(size_t key, const char* name) : BaseItem(key, name) {} +}; + +TEST_F(IntrusiveMultiMapTest, ConstIterator_CompareNonConst_CompilationFails) { + ::pw::IntrusiveMultiMap multimap; +#if PW_NC_TEST(CannotCompareIncompatibleIteratorsEqual) + PW_NC_EXPECT("multimap_\.end\(\) == multimap\.end\(\)"); + static_cast(multimap_.end() == multimap.end()); +#elif PW_NC_TEST(CannotCompareIncompatibleIteratorsInequal) + PW_NC_EXPECT("multimap_\.end\(\) != multimap\.end\(\)"); + static_cast(multimap_.end() != multimap.end()); +#endif // PW_NC_TEST +} + +#if PW_NC_TEST(CannotModifyThroughConstIterator) +PW_NC_EXPECT("function is not marked const|discards qualifiers"); + +TEST_F(IntrusiveMultiMapTest, ConstIterator_Modify) { + const IntrusiveMultiMap& multimap = multimap_; + auto iter = multimap.begin(); + iter->set_name("nope"); +} + +#endif // PW_NC_TEST + +// Capacity + +TEST_F(IntrusiveMultiMapTest, IsEmpty) { + const IntrusiveMultiMap& multimap = multimap_; + EXPECT_FALSE(multimap.empty()); + multimap_.clear(); + EXPECT_TRUE(multimap.empty()); +} + +TEST_F(IntrusiveMultiMapTest, GetSize) { + const IntrusiveMultiMap& multimap = multimap_; + EXPECT_EQ(multimap.size(), kNumItems); + multimap_.clear(); + EXPECT_EQ(multimap.size(), 0U); +} + +TEST_F(IntrusiveMultiMapTest, GetMaxSize) { + const IntrusiveMultiMap& multimap = multimap_; + EXPECT_EQ(multimap.max_size(), size_t(std::numeric_limits::max())); +} + +// Modifiers + +// This functions allows tests to use `std::is_sorted` without specializing +// `std::less`. Since `std::less` is the default value for the +// `Compare` template parameter, leaving it untouched avoids accidentally +// masking type-handling errors. +constexpr bool LessThan(const TestItem& lhs, const TestItem& rhs) { + return lhs.key() < rhs.key(); +} + +TEST_F(IntrusiveMultiMapTest, Insert) { + multimap_.clear(); + bool sorted = true; + size_t prev_key = 0; + for (auto& item : items_) { + sorted &= prev_key < item.key(); + + // Use the "hinted" version of insert. + multimap_.insert(multimap_.end(), item); + prev_key = item.key(); + } + EXPECT_FALSE(sorted); + + EXPECT_EQ(multimap_.size(), kNumItems); + EXPECT_TRUE(std::is_sorted(multimap_.begin(), multimap_.end(), LessThan)); +} + +TEST_F(IntrusiveMultiMapTest, Insert_Duplicate) { + TestItem item1(60, "1"); + TestItem item2(60, "2"); + + auto iter = multimap_.insert(item1); + EXPECT_STREQ(iter->name(), "1"); + + iter = multimap_.insert(item2); + EXPECT_STREQ(iter->name(), "2"); + + EXPECT_EQ(multimap_.size(), kNumItems + 2); + EXPECT_TRUE(std::is_sorted(multimap_.begin(), multimap_.end(), LessThan)); + + // Explicitly clear the multimap before item 1 goes out of scope. + multimap_.clear(); +} + +TEST_F(IntrusiveMultiMapTest, Insert_ObjectIterators) { + multimap_.clear(); + multimap_.insert(items_.begin(), items_.end()); + EXPECT_EQ(multimap_.size(), kNumItems); + EXPECT_TRUE(std::is_sorted(multimap_.begin(), multimap_.end(), LessThan)); +} + +TEST_F(IntrusiveMultiMapTest, Insert_ObjectIterators_Empty) { + multimap_.insert(items_.end(), items_.end()); + EXPECT_EQ(multimap_.size(), kNumItems); + EXPECT_TRUE(std::is_sorted(multimap_.begin(), multimap_.end(), LessThan)); +} + +TEST_F(IntrusiveMultiMapTest, Insert_ObjectIterators_WithDuplicates) { + std::array items = {{ + {50, "B"}, + {40, "D"}, + {60, "F"}, + }}; + + multimap_.insert(items.begin(), items.end()); + EXPECT_EQ(multimap_.size(), kNumItems + 3); + EXPECT_TRUE(std::is_sorted(multimap_.begin(), multimap_.end(), LessThan)); + + auto iter = multimap_.find(40); + ASSERT_NE(iter, multimap_.end()); + EXPECT_STREQ((iter++)->name(), "d"); + EXPECT_STREQ(iter->name(), "D"); + + iter = multimap_.find(50); + ASSERT_NE(iter, multimap_.end()); + EXPECT_STREQ((iter++)->name(), "b"); + EXPECT_STREQ(iter->name(), "B"); + + iter = multimap_.find(60); + ASSERT_NE(iter, multimap_.end()); + EXPECT_STREQ(iter->name(), "F"); + + // Explicitly clear the multimap before items goes out of scope. + multimap_.clear(); +} + +TEST_F(IntrusiveMultiMapTest, Insert_PointerIterators) { + multimap_.clear(); + std::array ptrs = {&items_[0], &items_[1], &items_[2]}; + + multimap_.insert(ptrs.begin(), ptrs.end()); + EXPECT_EQ(multimap_.size(), 3U); + EXPECT_TRUE(std::is_sorted(multimap_.begin(), multimap_.end(), LessThan)); +} + +TEST_F(IntrusiveMultiMapTest, Insert_PointerIterators_Empty) { + std::array ptrs; + + multimap_.insert(ptrs.begin(), ptrs.end()); + EXPECT_EQ(multimap_.size(), kNumItems); + EXPECT_TRUE(std::is_sorted(multimap_.begin(), multimap_.end(), LessThan)); +} + +TEST_F(IntrusiveMultiMapTest, Insert_PointerIterators_WithDuplicates) { + TestItem item1(50, "B"); + TestItem item2(40, "D"); + TestItem item3(60, "F"); + std::array ptrs = {&item1, &item2, &item3}; + + multimap_.insert(ptrs.begin(), ptrs.end()); + EXPECT_EQ(multimap_.size(), kNumItems + 3); + EXPECT_TRUE(std::is_sorted(multimap_.begin(), multimap_.end(), LessThan)); + + auto iter = multimap_.find(40); + ASSERT_NE(iter, multimap_.end()); + EXPECT_STREQ((iter++)->name(), "d"); + EXPECT_STREQ(iter->name(), "D"); + + iter = multimap_.find(50); + ASSERT_NE(iter, multimap_.end()); + EXPECT_STREQ((iter++)->name(), "b"); + EXPECT_STREQ(iter->name(), "B"); + + iter = multimap_.find(60); + ASSERT_NE(iter, multimap_.end()); + EXPECT_STREQ(iter->name(), "F"); + + // Explicitly clear the multimap before items goes out of scope. + multimap_.clear(); +} + +TEST_F(IntrusiveMultiMapTest, Insert_InitializerList) { + multimap_.clear(); + multimap_.insert({&items_[0], &items_[2], &items_[4]}); + EXPECT_EQ(multimap_.size(), 3U); + EXPECT_TRUE(std::is_sorted(multimap_.begin(), multimap_.end(), LessThan)); +} + +TEST_F(IntrusiveMultiMapTest, Insert_InitializerList_Empty) { + multimap_.insert({}); + EXPECT_EQ(multimap_.size(), kNumItems); + EXPECT_TRUE(std::is_sorted(multimap_.begin(), multimap_.end(), LessThan)); +} + +TEST_F(IntrusiveMultiMapTest, Insert_InitializerList_WithDuplicates) { + TestItem item1(50, "B"); + TestItem item2(40, "D"); + TestItem item3(60, "F"); + + multimap_.insert({&item1, &item2, &item3}); + EXPECT_EQ(multimap_.size(), kNumItems + 3); + EXPECT_TRUE(std::is_sorted(multimap_.begin(), multimap_.end(), LessThan)); + + auto iter = multimap_.find(40); + ASSERT_NE(iter, multimap_.end()); + EXPECT_STREQ((iter++)->name(), "d"); + EXPECT_STREQ(iter->name(), "D"); + + iter = multimap_.find(50); + ASSERT_NE(iter, multimap_.end()); + EXPECT_STREQ((iter++)->name(), "b"); + EXPECT_STREQ(iter->name(), "B"); + + iter = multimap_.find(60); + ASSERT_NE(iter, multimap_.end()); + EXPECT_STREQ(iter->name(), "F"); + + // Explicitly clear the multimap before items goes out of scope. + multimap_.clear(); +} + +// An item derived from TestItem. +struct DerivedItem : public TestItem { + DerivedItem(size_t n, const char* name) : TestItem(n * 10, name) {} +}; + +TEST_F(IntrusiveMultiMapTest, Insert_DerivedItems) { + DerivedItem item1(6, "f"); + multimap_.insert(item1); + + DerivedItem item2(7, "g"); + multimap_.insert(item2); + + EXPECT_EQ(multimap_.size(), kNumItems + 2); + EXPECT_TRUE(std::is_sorted(multimap_.begin(), multimap_.end(), LessThan)); + + // Explicitly clear the multimap before items goes out of scope. + multimap_.clear(); +} + +TEST_F(IntrusiveMultiMapTest, Insert_DerivedItems_CompilationFails) { + ::pw::IntrusiveMultiMap + derived_from_compatible_item_type; + + DerivedItem item1(6, "f"); + derived_from_compatible_item_type.insert(item1); + + EXPECT_EQ(derived_from_compatible_item_type.size(), 1U); + +#if PW_NC_TEST(CannotAddBaseClassToDerivedClassMap) + PW_NC_EXPECT("derived_from_compatible_item_type\.insert\(item2\)"); + + TestItem item2(70, "g"); + derived_from_compatible_item_type.insert(item2); +#endif + derived_from_compatible_item_type.clear(); +} + +TEST_F(IntrusiveMultiMapTest, Erase_OneItem) { + EXPECT_EQ(multimap_.size(), kNumItems); + EXPECT_EQ(multimap_.erase(items_[2].key()), 1U); + EXPECT_EQ(multimap_.size(), kNumItems - 1); + + auto iter = multimap_.find(items_[2].key()); + EXPECT_EQ(iter, multimap_.end()); +} + +TEST_F(IntrusiveMultiMapTest, Erase_OnlyItem) { + multimap_.clear(); + multimap_.insert(items_[0]); + EXPECT_EQ(multimap_.size(), 1U); + + EXPECT_EQ(multimap_.erase(items_[0].key()), 1U); + EXPECT_EQ(multimap_.size(), 0U); +} + +TEST_F(IntrusiveMultiMapTest, Erase_AllOnebyOne) { + auto iter = multimap_.begin(); + for (size_t n = kNumItems; n != 0; --n) { + ASSERT_NE(iter, multimap_.end()); + iter = multimap_.erase(iter); + } + EXPECT_EQ(iter, multimap_.end()); + EXPECT_EQ(multimap_.size(), 0U); +} + +TEST_F(IntrusiveMultiMapTest, Erase_Range) { + auto first = multimap_.begin(); + auto last = multimap_.end(); + ++first; + --last; + auto iter = multimap_.erase(first, last); + EXPECT_EQ(multimap_.size(), 2U); + EXPECT_TRUE(std::is_sorted(multimap_.begin(), multimap_.end(), LessThan)); + EXPECT_EQ(iter->key(), 50U); +} + +TEST_F(IntrusiveMultiMapTest, Erase_MissingItem) { + EXPECT_EQ(multimap_.erase(100), 0U); +} + +TEST_F(IntrusiveMultiMapTest, Erase_Reinsert) { + EXPECT_EQ(multimap_.size(), items_.size()); + + EXPECT_EQ(multimap_.erase(items_[0].key()), 1U); + EXPECT_EQ(multimap_.find(items_[0].key()), multimap_.end()); + + EXPECT_EQ(multimap_.erase(items_[2].key()), 1U); + EXPECT_EQ(multimap_.find(items_[2].key()), multimap_.end()); + + EXPECT_EQ(multimap_.erase(items_[4].key()), 1U); + EXPECT_EQ(multimap_.find(items_[4].key()), multimap_.end()); + + EXPECT_EQ(multimap_.size(), items_.size() - 3); + + multimap_.insert(items_[4]); + auto iter = multimap_.find(items_[4].key()); + EXPECT_NE(iter, multimap_.end()); + + multimap_.insert(items_[0]); + iter = multimap_.find(items_[0].key()); + EXPECT_NE(iter, multimap_.end()); + + multimap_.insert(items_[2]); + iter = multimap_.find(items_[2].key()); + EXPECT_NE(iter, multimap_.end()); + + EXPECT_EQ(multimap_.size(), items_.size()); +} + +TEST_F(IntrusiveMultiMapTest, Erase_Duplicate) { + TestItem item1(35, "1"); + TestItem item2(35, "2"); + TestItem item3(35, "3"); + multimap_.insert(item1); + multimap_.insert(item2); + multimap_.insert(item3); + + auto iter = multimap_.find(35); + ASSERT_NE(iter, multimap_.end()); + EXPECT_STREQ(iter->name(), "1"); + + iter = multimap_.erase(iter); + ASSERT_NE(iter, multimap_.end()); + EXPECT_STREQ(iter->name(), "2"); + + iter = multimap_.erase(iter); + ASSERT_NE(iter, multimap_.end()); + EXPECT_STREQ(iter->name(), "3"); + + multimap_.erase(iter); + EXPECT_EQ(multimap_.find(35), multimap_.end()); +} + +TEST_F(IntrusiveMultiMapTest, Swap) { + std::array items = {{ + {50, "B"}, + {40, "D"}, + {60, "F"}, + }}; + IntrusiveMultiMap multimap(items.begin(), items.end()); + + multimap_.swap(multimap); + EXPECT_EQ(multimap.size(), kNumItems); + EXPECT_TRUE(std::is_sorted(multimap.begin(), multimap.end(), LessThan)); + auto iter = multimap.begin(); + EXPECT_STREQ((iter++)->name(), "e"); + EXPECT_STREQ((iter++)->name(), "c"); + EXPECT_STREQ((iter++)->name(), "a"); + EXPECT_STREQ((iter++)->name(), "d"); + EXPECT_STREQ((iter++)->name(), "b"); + EXPECT_EQ(iter, multimap.end()); + multimap.clear(); + + EXPECT_EQ(multimap_.size(), 3U); + EXPECT_TRUE(std::is_sorted(multimap_.begin(), multimap_.end(), LessThan)); + iter = multimap_.begin(); + EXPECT_STREQ((iter++)->name(), "D"); + EXPECT_STREQ((iter++)->name(), "B"); + EXPECT_STREQ((iter++)->name(), "F"); + EXPECT_EQ(iter, multimap_.end()); + + // Explicitly clear the multimap before items goes out of scope. + multimap_.clear(); +} + +TEST_F(IntrusiveMultiMapTest, Swap_Empty) { + IntrusiveMultiMap multimap; + + multimap_.swap(multimap); + EXPECT_EQ(multimap.size(), kNumItems); + EXPECT_TRUE(std::is_sorted(multimap.begin(), multimap.end(), LessThan)); + auto iter = multimap.begin(); + EXPECT_STREQ((iter++)->name(), "e"); + EXPECT_STREQ((iter++)->name(), "c"); + EXPECT_STREQ((iter++)->name(), "a"); + EXPECT_STREQ((iter++)->name(), "d"); + EXPECT_STREQ((iter++)->name(), "b"); + EXPECT_EQ(iter, multimap.end()); + multimap.clear(); + + EXPECT_EQ(multimap_.size(), 0U); +} + +TEST_F(IntrusiveMultiMapTest, Merge) { + std::array items = {{ + {15, "f"}, + {45, "g"}, + {65, "h"}, + }}; + IntrusiveMultiMap multimap(items.begin(), items.end()); + + multimap_.merge(multimap); + EXPECT_TRUE(multimap.empty()); + EXPECT_EQ(multimap_.size(), kNumItems + 3); + EXPECT_TRUE(std::is_sorted(multimap_.begin(), multimap_.end(), LessThan)); + auto iter = multimap_.begin(); + EXPECT_STREQ((iter++)->name(), "e"); + EXPECT_STREQ((iter++)->name(), "f"); + EXPECT_STREQ((iter++)->name(), "c"); + EXPECT_STREQ((iter++)->name(), "a"); + EXPECT_STREQ((iter++)->name(), "d"); + EXPECT_STREQ((iter++)->name(), "g"); + EXPECT_STREQ((iter++)->name(), "b"); + EXPECT_STREQ((iter++)->name(), "h"); + EXPECT_EQ(iter, multimap_.end()); + + // Explicitly clear the multimap before items goes out of scope. + multimap_.clear(); +} + +TEST_F(IntrusiveMultiMapTest, Merge_Empty) { + IntrusiveMultiMap multimap; + + multimap_.merge(multimap); + EXPECT_EQ(multimap_.size(), kNumItems); + EXPECT_TRUE(std::is_sorted(multimap_.begin(), multimap_.end(), LessThan)); + + multimap.merge(multimap_); + EXPECT_TRUE(multimap_.empty()); + EXPECT_EQ(multimap.size(), kNumItems); + EXPECT_TRUE(std::is_sorted(multimap.begin(), multimap.end(), LessThan)); + + multimap.clear(); +} + +TEST_F(IntrusiveMultiMapTest, Merge_WithDuplicates) { + std::array items = {{ + {50, "B"}, + {40, "D"}, + {60, "F"}, + }}; + IntrusiveMultiMap multimap(items.begin(), items.end()); + + multimap_.merge(multimap); + EXPECT_TRUE(multimap.empty()); + EXPECT_EQ(multimap_.size(), kNumItems + 3); + EXPECT_TRUE(std::is_sorted(multimap_.begin(), multimap_.end(), LessThan)); + auto iter = multimap_.begin(); + EXPECT_STREQ((iter++)->name(), "e"); + EXPECT_STREQ((iter++)->name(), "c"); + EXPECT_STREQ((iter++)->name(), "a"); + EXPECT_STREQ((iter++)->name(), "d"); + EXPECT_STREQ((iter++)->name(), "D"); + EXPECT_STREQ((iter++)->name(), "b"); + EXPECT_STREQ((iter++)->name(), "B"); + EXPECT_STREQ((iter++)->name(), "F"); + EXPECT_EQ(iter, multimap_.end()); + + // Explicitly clear the multimap before items goes out of scope. + multimap_.clear(); +} + +// A struct for a map instead of a multimap. +struct MapItem : public ::pw::IntrusiveMap::Item, + public BaseItem { + MapItem(size_t key, const char* name) : BaseItem(key, name) {} +}; + +TEST_F(IntrusiveMultiMapTest, Merge_Map) { + std::array items = {{ + {50, "B"}, + {40, "D"}, + {60, "F"}, + }}; + ::pw::IntrusiveMap map(items.begin(), items.end()); + + multimap_.merge(map); + EXPECT_TRUE(map.empty()); + EXPECT_EQ(multimap_.size(), kNumItems + 3); + EXPECT_TRUE(std::is_sorted(multimap_.begin(), multimap_.end(), LessThan)); + auto iter = multimap_.begin(); + EXPECT_STREQ((iter++)->name(), "e"); + EXPECT_STREQ((iter++)->name(), "c"); + EXPECT_STREQ((iter++)->name(), "a"); + EXPECT_STREQ((iter++)->name(), "d"); + EXPECT_STREQ((iter++)->name(), "D"); + EXPECT_STREQ((iter++)->name(), "b"); + EXPECT_STREQ((iter++)->name(), "B"); + EXPECT_STREQ((iter++)->name(), "F"); + EXPECT_EQ(iter, multimap_.end()); + + // Explicitly clear the multimap before items goes out of scope. + multimap_.clear(); +} + +TEST_F(IntrusiveMultiMapTest, Count) { + const IntrusiveMultiMap& multimap = multimap_; + EXPECT_EQ(multimap.count(10), 1U); + EXPECT_EQ(multimap.count(20), 1U); + EXPECT_EQ(multimap.count(30), 1U); + EXPECT_EQ(multimap.count(40), 1U); + EXPECT_EQ(multimap.count(50), 1U); +} + +TEST_F(IntrusiveMultiMapTest, Count_NoSuchKey) { + const IntrusiveMultiMap& multimap = multimap_; + EXPECT_EQ(multimap.count(60), 0U); +} + +TEST_F(IntrusiveMultiMapTest, Count_WithDuplicates) { + std::array items = {{ + {50, "B"}, + {40, "D"}, + {60, "F"}, + }}; + multimap_.insert(items.begin(), items.end()); + + EXPECT_EQ(multimap_.count(10), 1U); + EXPECT_EQ(multimap_.count(20), 1U); + EXPECT_EQ(multimap_.count(30), 1U); + EXPECT_EQ(multimap_.count(40), 2U); + EXPECT_EQ(multimap_.count(50), 2U); + EXPECT_EQ(multimap_.count(60), 1U); + + // Explicitly clear the multimap before items goes out of scope. + multimap_.clear(); +} + +TEST_F(IntrusiveMultiMapTest, Find) { + const IntrusiveMultiMap& multimap = multimap_; + size_t key = 0; + for (size_t i = 0; i < kNumItems; ++i) { + key += 10; + auto iter = multimap.find(key); + ASSERT_NE(iter, multimap.end()); + EXPECT_EQ(iter->key(), key); + } +} + +TEST_F(IntrusiveMultiMapTest, Find_NoSuchKey) { + const IntrusiveMultiMap& multimap = multimap_; + auto iter = multimap.find(45); + EXPECT_EQ(iter, multimap.end()); +} + +TEST_F(IntrusiveMultiMapTest, Find_WithDuplicates) { + std::array items = {{ + {50, "B"}, + {40, "D"}, + {60, "F"}, + }}; + multimap_.insert(items.begin(), items.end()); + + auto iter = multimap_.find(40); + ASSERT_NE(iter, multimap_.end()); + EXPECT_EQ(iter->key(), 40U); + EXPECT_EQ((iter++)->name(), "d"); + EXPECT_EQ(iter->key(), 40U); + EXPECT_EQ(iter->name(), "D"); + + iter = multimap_.find(50); + ASSERT_NE(iter, multimap_.end()); + EXPECT_EQ(iter->key(), 50U); + EXPECT_EQ((iter++)->name(), "b"); + EXPECT_EQ(iter->key(), 50U); + EXPECT_EQ(iter->name(), "B"); + + // Explicitly clear the multimap before items goes out of scope. + multimap_.clear(); +} + +TEST_F(IntrusiveMultiMapTest, LowerBound) { + const IntrusiveMultiMap& multimap = multimap_; + auto iter = multimap.lower_bound(10); + ASSERT_NE(iter, multimap.end()); + EXPECT_STREQ(iter->name(), "e"); + + iter = multimap.lower_bound(20); + ASSERT_NE(iter, multimap.end()); + EXPECT_STREQ(iter->name(), "c"); + + iter = multimap.lower_bound(30); + ASSERT_NE(iter, multimap.end()); + EXPECT_STREQ(iter->name(), "a"); + + iter = multimap.lower_bound(40); + ASSERT_NE(iter, multimap.end()); + EXPECT_STREQ(iter->name(), "d"); + + iter = multimap.lower_bound(50); + ASSERT_NE(iter, multimap.end()); + EXPECT_STREQ(iter->name(), "b"); +} + +TEST_F(IntrusiveMultiMapTest, LowerBound_NoExactKey) { + const IntrusiveMultiMap& multimap = multimap_; + auto iter = multimap.lower_bound(5); + ASSERT_NE(iter, multimap.end()); + EXPECT_STREQ(iter->name(), "e"); + + iter = multimap.lower_bound(15); + ASSERT_NE(iter, multimap.end()); + EXPECT_STREQ(iter->name(), "c"); + + iter = multimap.lower_bound(25); + ASSERT_NE(iter, multimap.end()); + EXPECT_STREQ(iter->name(), "a"); + + iter = multimap.lower_bound(35); + ASSERT_NE(iter, multimap.end()); + EXPECT_STREQ(iter->name(), "d"); + + iter = multimap.lower_bound(45); + ASSERT_NE(iter, multimap.end()); + EXPECT_STREQ(iter->name(), "b"); +} + +TEST_F(IntrusiveMultiMapTest, LowerBound_OutOfRange) { + const IntrusiveMultiMap& multimap = multimap_; + EXPECT_EQ(multimap.lower_bound(55), multimap.end()); +} + +TEST_F(IntrusiveMultiMapTest, LowerBound_WithDuplicates) { + TestItem item1(20, "1"); + TestItem item2(40, "1"); + TestItem item3(40, "1"); + multimap_.insert(item1); + multimap_.insert(item2); + multimap_.insert(item3); + EXPECT_EQ(multimap_.size(), items_.size() + 3); + + auto iter = multimap_.lower_bound(20); + EXPECT_LT((--iter)->key(), 20U); + EXPECT_EQ((++iter)->key(), 20U); + EXPECT_EQ((++iter)->key(), 20U); + EXPECT_GT((++iter)->key(), 20U); + + iter = multimap_.lower_bound(40); + EXPECT_LT((--iter)->key(), 40U); + EXPECT_EQ((++iter)->key(), 40U); + EXPECT_EQ((++iter)->key(), 40U); + EXPECT_EQ((++iter)->key(), 40U); + EXPECT_GT((++iter)->key(), 40U); + + // Explicitly clear the multimap before items 1-3 go out of scope. + multimap_.clear(); +} + +TEST_F(IntrusiveMultiMapTest, UpperBound) { + const IntrusiveMultiMap& multimap = multimap_; + auto iter = multimap.upper_bound(10); + ASSERT_NE(iter, multimap.end()); + EXPECT_STREQ(iter->name(), "c"); + + iter = multimap.upper_bound(20); + ASSERT_NE(iter, multimap.end()); + EXPECT_STREQ(iter->name(), "a"); + + iter = multimap.upper_bound(30); + ASSERT_NE(iter, multimap.end()); + EXPECT_STREQ(iter->name(), "d"); + + iter = multimap.upper_bound(40); + ASSERT_NE(iter, multimap.end()); + EXPECT_STREQ(iter->name(), "b"); + + EXPECT_EQ(multimap.upper_bound(50), multimap.end()); +} + +TEST_F(IntrusiveMultiMapTest, UpperBound_NoExactKey) { + const IntrusiveMultiMap& multimap = multimap_; + auto iter = multimap.upper_bound(5); + ASSERT_NE(iter, multimap.end()); + EXPECT_STREQ(iter->name(), "e"); + + iter = multimap.upper_bound(15); + ASSERT_NE(iter, multimap.end()); + EXPECT_STREQ(iter->name(), "c"); + + iter = multimap.upper_bound(25); + ASSERT_NE(iter, multimap.end()); + EXPECT_STREQ(iter->name(), "a"); + + iter = multimap.upper_bound(35); + ASSERT_NE(iter, multimap.end()); + EXPECT_STREQ(iter->name(), "d"); + + iter = multimap.upper_bound(45); + ASSERT_NE(iter, multimap.end()); + EXPECT_STREQ(iter->name(), "b"); +} + +TEST_F(IntrusiveMultiMapTest, UpperBound_OutOfRange) { + const IntrusiveMultiMap& multimap = multimap_; + EXPECT_EQ(multimap.upper_bound(55), multimap.end()); +} + +TEST_F(IntrusiveMultiMapTest, UpperBound_WithDuplicates) { + TestItem item1(20, "1"); + TestItem item2(40, "1"); + TestItem item3(40, "1"); + multimap_.insert(item1); + multimap_.insert(item2); + multimap_.insert(item3); + EXPECT_EQ(multimap_.size(), items_.size() + 3); + + auto iter = multimap_.upper_bound(20); + EXPECT_GT(iter->key(), 20U); + + iter = multimap_.upper_bound(40); + EXPECT_GT(iter->key(), 40U); + + // Explicitly clear the multimap before items 1-3 go out of scope. + multimap_.clear(); +} + +TEST_F(IntrusiveMultiMapTest, EqualRange) { + const IntrusiveMultiMap& multimap = multimap_; + + auto pair = multimap.equal_range(10); + IntrusiveMultiMap::const_iterator lower = pair.first; + IntrusiveMultiMap::const_iterator upper = pair.second; + ASSERT_NE(lower, multimap.end()); + EXPECT_STREQ(lower->name(), "e"); + ASSERT_NE(upper, multimap.end()); + EXPECT_STREQ(upper->name(), "c"); + + std::tie(lower, upper) = multimap.equal_range(20); + ASSERT_NE(lower, multimap.end()); + EXPECT_STREQ(lower->name(), "c"); + ASSERT_NE(upper, multimap.end()); + EXPECT_STREQ(upper->name(), "a"); + + std::tie(lower, upper) = multimap.equal_range(30); + ASSERT_NE(lower, multimap.end()); + EXPECT_STREQ(lower->name(), "a"); + ASSERT_NE(upper, multimap.end()); + EXPECT_STREQ(upper->name(), "d"); + + std::tie(lower, upper) = multimap.equal_range(40); + ASSERT_NE(lower, multimap.end()); + EXPECT_STREQ(lower->name(), "d"); + ASSERT_NE(upper, multimap.end()); + EXPECT_STREQ(upper->name(), "b"); + + std::tie(lower, upper) = multimap.equal_range(50); + ASSERT_NE(lower, multimap.end()); + EXPECT_STREQ(lower->name(), "b"); + EXPECT_EQ(upper, multimap.end()); +} + +TEST_F(IntrusiveMultiMapTest, EqualRange_NoExactKey) { + const IntrusiveMultiMap& multimap = multimap_; + + auto pair = multimap.equal_range(5); + IntrusiveMultiMap::const_iterator lower = pair.first; + IntrusiveMultiMap::const_iterator upper = pair.second; + ASSERT_NE(lower, multimap.end()); + EXPECT_STREQ(lower->name(), "e"); + ASSERT_NE(upper, multimap.end()); + EXPECT_STREQ(upper->name(), "e"); + + std::tie(lower, upper) = multimap.equal_range(15); + ASSERT_NE(lower, multimap.end()); + EXPECT_STREQ(lower->name(), "c"); + ASSERT_NE(upper, multimap.end()); + EXPECT_STREQ(upper->name(), "c"); + + std::tie(lower, upper) = multimap.equal_range(25); + ASSERT_NE(lower, multimap.end()); + EXPECT_STREQ(lower->name(), "a"); + ASSERT_NE(upper, multimap.end()); + EXPECT_STREQ(upper->name(), "a"); + + std::tie(lower, upper) = multimap.equal_range(35); + ASSERT_NE(lower, multimap.end()); + EXPECT_STREQ(lower->name(), "d"); + ASSERT_NE(upper, multimap.end()); + EXPECT_STREQ(upper->name(), "d"); + + std::tie(lower, upper) = multimap.equal_range(45); + ASSERT_NE(lower, multimap.end()); + EXPECT_STREQ(lower->name(), "b"); + ASSERT_NE(upper, multimap.end()); + EXPECT_STREQ(upper->name(), "b"); +} + +TEST_F(IntrusiveMultiMapTest, EqualRange_OutOfRange) { + const IntrusiveMultiMap& multimap = multimap_; + + auto pair = multimap.equal_range(55); + IntrusiveMultiMap::const_iterator lower = pair.first; + IntrusiveMultiMap::const_iterator upper = pair.second; + EXPECT_EQ(lower, multimap.end()); + EXPECT_EQ(upper, multimap.end()); +} + +TEST_F(IntrusiveMultiMapTest, EqualRange_WithDuplicates) { + TestItem item1(40, "1"); + TestItem item2(40, "2"); + TestItem item3(40, "3"); + multimap_.insert(item1); + multimap_.insert(item2); + multimap_.insert(item3); + + auto result = multimap_.equal_range(40); + EXPECT_EQ(std::distance(result.first, result.second), 4); + + // Explicitly clear the multimap before items 1-3 go out of scope. + multimap_.clear(); +} + +} // namespace diff --git a/pw_containers/public/pw_containers/internal/aa_tree.h b/pw_containers/public/pw_containers/internal/aa_tree.h new file mode 100644 index 0000000000..db0156bce4 --- /dev/null +++ b/pw_containers/public/pw_containers/internal/aa_tree.h @@ -0,0 +1,390 @@ +// Copyright 2024 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +#pragma once + +#include +#include +#include +#include + +#include "pw_assert/assert.h" +#include "pw_containers/internal/aa_tree_item.h" +#include "pw_containers/internal/aa_tree_iterator.h" +#include "pw_containers/internal/intrusive.h" + +namespace pw::containers::internal { + +// Forward declaration for friending. +template +class AATree; + +/// Base type for an AA tree that is devoid of template parameters. +/// +/// This generic class does not implement any functionality that requires +/// comparing keys, and should not be used directly. Instead, see the `AATree` +/// class that is templated on methods to get and compare keys. +class GenericAATree { + public: + using iterator = AATreeIterator<>; + + /// Constructs an empty AA tree. + /// + /// @param unique_keys Indicates if this tree requires unique keys (as with + /// `std::map`) or allows duplicate keys (as with + /// `std::multimap`). + constexpr explicit GenericAATree(bool unique_keys) + : unique_keys_(unique_keys) {} + + /// Destructor. + ~GenericAATree() { CheckIntrusiveContainerIsEmpty(empty()); } + + // Intrusive maps cannot be copied, since each Item can only be in one tree. + GenericAATree(const GenericAATree&) = delete; + GenericAATree& operator=(const GenericAATree&) = delete; + + [[nodiscard]] constexpr bool unique_keys() const { return unique_keys_; } + + /// Sets the tree's root item. + void SetRoot(AATreeItem* item); + + // Iterators + + /// Returns a pointer to the first item, if any. + constexpr iterator begin() noexcept { + return empty() ? iterator(&root_) : iterator(&root_, root_->GetLeftmost()); + } + + /// Returns a pointer to the last item, if any. + constexpr iterator end() noexcept { + return empty() ? iterator(&root_) + : ++(iterator(&root_, root_->GetRightmost())); + } + + // Capacity + + /// Returns whether the tree has zero items or not. + [[nodiscard]] constexpr bool empty() const { return root_ == nullptr; } + + /// Returns the number of items in the tree. + size_t size() const; + + /// Returns how many items can be added. + /// + /// As an intrusive container, this is effectively unbounded. + constexpr size_t max_size() const noexcept { + return std::numeric_limits::max(); + } + + // Modifiers + + /// Removes all items from the tree and leaves it empty. + /// + /// The items themselves are not destructed. + void clear(); + + /// Removes an item from the tree and returns an iterator to the item after + /// the removed item.. + /// + /// The items themselves are not destructed. + iterator erase(AATreeItem& item); + + /// Removes the items from first, inclusive, to last, exclusive from the tree + /// and returns an iterator to the item after the last removed item. + /// + /// The items themselves are not destructed. + iterator erase(AATreeItem& first, AATreeItem& last); + + /// Exchanges this tree's items with the `other` tree's items. + void swap(GenericAATree& other); + + private: + // The derived, templated type needs to be able to access the root element. + template + friend class AATree; + + /// Root of the tree. Only null if the tree is empty. + AATreeItem* root_ = nullptr; + + /// Indicates whether the tree requires unique keys. + /// + /// This is a const member rather than a template parameter for 3 reasons: + /// 1. It is a tree field and not an item field, meaning the space overhead is + /// marginal. + /// 2. It is only used in a single branch, i.e. when inserting an item with a + /// duplicate key. Making this branch constexpr-if would only save a + /// minimal amount of code size. + /// 3. It allows the same types of ``AATree``, ``AATree::Item`` and + /// ``iterator`` to be used for both ``IntrusiveMap`` and + /// ``IntrusiveMultiMap``, thereby reducing code size more significantly. + const bool unique_keys_; +}; + +/// An AA tree, as described by Arne Andersson in +/// https://user.it.uu.se/~arneande/ps/simp.pdf. AA trees are simplified +/// red-black which offer almost as much performance with much simpler and +/// smaller code. +/// +/// The tree provides an ordered collection of keyed items, and is used to +/// implement ``pw::IntrusiveMap`` and ``pw::IntrusiveMultiMap``. Keys are +/// retrieved and compared using the functions provided via template parameters. +/// +/// @tparam GetKey Function with signature `Key(const Item&)` that +/// returns the value that items are sorted on. +/// @tparam Compare Function with the signature `bool(Key, Key) that is +/// used to order items. +template +class AATree final : public GenericAATree { + public: + using GenericAATree::iterator; + using Key = std::invoke_result_t; + + /// Constructs an empty AA tree. + /// + /// @param unique_keys Indicates if this tree requires unique keys (as with + /// `std::map`) or allows duplicate keys (as with + /// `std::multimap`). + constexpr explicit AATree(bool unique_keys) : GenericAATree(unique_keys) {} + + // Modifiers + + /// Attempts to add the given item to the tree. + /// + /// The item will be added if the tree does not already contain an item with + /// the given item's key or if the tree does not require unique keys. + /// + /// @pre The item must not be a part of any map. + /// + /// @returns A pointer to the inserted item and `true`, or a pointer to the + /// existing item with same key and `false`. + std::pair insert(AATreeItem& item); + + /// Inserts each item from `first`, inclusive, to `last`, exclusive. If the + /// tree does not all duplicates and an equivalent item is already in the + /// tree, the item is ignored. + template + void insert(Iterator first, Iterator last); + + /// @copydoc GenericAATree::erase + using GenericAATree::erase; + + /// Removes items that match the given `key`, and returns the number of items + /// removed. + size_t erase(const Key& key); + + /// Splices items from the `other` map into this one. + /// + /// The receiving map's `GetKey` and `Compare` functions are used when + /// inserting items. + template + void merge(AATree& other); + + // Lookup + + /// Returns the number of items in the map with the given key. + /// + /// If the map requires unique keys, this simply 0 or 1. + size_t count(const Key& key); + + /// Returns a pointer to an item with the given key, or null if the tree does + /// contain such an item. + iterator find(const Key& key); + + /// Returns a pair of items where the first points to the item with the + /// smallest key that is not less than the given key, and the second points to + /// the item with the smallest key that is greater than the given key. + std::pair equal_range(const Key& key); + + /// Returns the item in the tree with the smallest key that is greater than or + /// equal to the given key, or null if the tree is empty. + iterator lower_bound(const Key& key); + + /// Returns the item in the tree with the smallest key that is strictly + /// greater than the given key, or null if the tree is empty. + iterator upper_bound(const Key& key); + + private: + // Concrete instantiation of functors, using function-name style. + GetKey get_key; + Compare key_compare; + + /// Inserts the given `child` in the subtree rooted by `parent` and returns + /// the resulting tree. If the tree does not allow duplicates and an + /// equivalent item is already in the tree, the tree is unchanged and the + /// existing item is returned via `duplicate`. + AATreeItem* InsertImpl(AATreeItem& parent, + AATreeItem& child, + AATreeItem*& duplicate); + + /// Returns the item in the subtree rooted by the given `item` with the + /// smallest key that is greater than or equal to the given `key`, or null if + /// the subtree is empty. + AATreeItem* GetLowerBoundImpl(AATreeItem* item, const Key& key); + + /// Returns the item in the subtree rooted by the given `item` with the + /// smallest key that is strictly greater than the given `key`, or null if the + /// subtree is empty. + AATreeItem* GetUpperBoundImpl(AATreeItem* item, const Key& key); +}; + +// Template method implementations. + +template +std::pair AATree::insert( + AATreeItem& item) { + CheckIntrusiveItemIsUncontained(!item.IsMapped()); + item.SetLevel(1); + if (empty()) { + SetRoot(&item); + return std::make_pair(iterator(&root_, &item), true); + } + AATreeItem* duplicate = nullptr; + SetRoot(InsertImpl(*root_, item, duplicate)); + if (duplicate == nullptr) { + return std::make_pair(iterator(&root_, &item), true); + } + item.Reset(); + return std::make_pair(iterator(&root_, duplicate), false); +} + +template +AATreeItem* AATree::InsertImpl(AATreeItem& parent, + AATreeItem& child, + AATreeItem*& duplicate) { + if (key_compare(get_key(child), get_key(parent))) { + AATreeItem* left = parent.left_.get(); + left = left == nullptr ? &child : InsertImpl(*left, child, duplicate); + parent.SetLeft(left); + + } else if (key_compare(get_key(parent), get_key(child)) || !unique_keys()) { + AATreeItem* right = parent.right_.get(); + right = right == nullptr ? &child : InsertImpl(*right, child, duplicate); + parent.SetRight(right); + + } else { + duplicate = &parent; + return &parent; + } + + return parent.Skew()->Split(); +} + +template +template +void AATree::insert(Iterator first, Iterator last) { + for (Iterator iter = first; iter != last; ++iter) { + if constexpr (std::is_pointer_v< + typename std::iterator_traits::value_type>) { + insert(**iter); + } else { + insert(*iter); + } + } +} + +template +size_t AATree::erase(const Key& key) { + size_t removed = 0; + auto iter = begin(); + while (iter != end()) { + auto& item = *iter++; + if (!key_compare(get_key(item), key) && !key_compare(key, get_key(item))) { + GenericAATree::erase(item); + ++removed; + } + } + return removed; +} + +template +template +void AATree::merge(AATree& other) { + auto iter = other.begin(); + while (!other.empty()) { + AATreeItem& item = *iter; + iter = other.erase(item); + insert(item); + } +} + +template +size_t AATree::count(const Key& key) { + return std::distance(lower_bound(key), upper_bound(key)); +} + +template +GenericAATree::iterator AATree::find(const Key& key) { + iterator iter = lower_bound(key); + if (iter == end() || key_compare(key, get_key(*iter))) { + return end(); + } + return iter; +} + +template +std::pair +AATree::equal_range(const Key& key) { + return std::make_pair(lower_bound(key), upper_bound(key)); +} + +template +GenericAATree::iterator AATree::lower_bound(const Key& key) { + AATreeItem* item = GetLowerBoundImpl(root_, key); + return item == nullptr ? end() : iterator(&root_, item); +} + +template +AATreeItem* AATree::GetLowerBoundImpl(AATreeItem* item, + const Key& key) { + if (item == nullptr) { + return nullptr; + } + // If the item's key is less than the key, go right. + if (key_compare(get_key(*item), key)) { + return GetLowerBoundImpl(item->right_.get(), key); + } + // Otherwise the item's key is greater than the key. Try to go left. + AATreeItem* left = item->left_.get(); + if (left == nullptr) { + return item; + } + AATreeItem* next = GetLowerBoundImpl(left, key); + return next == nullptr ? item : next; +} + +template +GenericAATree::iterator AATree::upper_bound(const Key& key) { + AATreeItem* item = GetUpperBoundImpl(root_, key); + return item == nullptr ? end() : iterator(&root_, item); +} + +template +AATreeItem* AATree::GetUpperBoundImpl(AATreeItem* item, + const Key& key) { + if (item == nullptr) { + return nullptr; + } + // If the item's key is less than or equal to the key, go right. + if (!key_compare(key, get_key(*item))) { + return GetUpperBoundImpl(item->right_.get(), key); + } + // Otherwise the item's key is greater than the key. Try to go left. + AATreeItem* left = item->left_.get(); + if (left == nullptr) { + return item; + } + AATreeItem* next = GetUpperBoundImpl(left, key); + return next == nullptr ? item : next; +} + +} // namespace pw::containers::internal diff --git a/pw_containers/public/pw_containers/internal/aa_tree_item.h b/pw_containers/public/pw_containers/internal/aa_tree_item.h new file mode 100644 index 0000000000..84eeecca88 --- /dev/null +++ b/pw_containers/public/pw_containers/internal/aa_tree_item.h @@ -0,0 +1,154 @@ +// Copyright 2024 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +#pragma once + +#include + +#include "pw_bytes/packed_ptr.h" + +namespace pw::containers::internal { + +// Forward declaration for friending. +template +class AATreeIterator; + +/// Base type for items stored in an AA tree, as described by Arne Andersson in +/// https://user.it.uu.se/~arneande/ps/simp.pdf. AA trees are simplified +/// red-black which offer almost as much performance with much simpler and +/// smaller code. +/// +/// The major difference between the nodes described by Andersson and this +/// implementation is the addition of a back-reference from an item to its +/// parent, allowing additional methods that operate on ranges of nodes. This +/// allows methods that more closely match the interfaces of `std::map` and +/// `std::multimap`. +class AATreeItem { + public: + /// Destructor. An item cannot be part of a tree when it is destroyed. + ~AATreeItem() { PW_ASSERT(!IsMapped()); } + + // AATreeItems are not copyable. + AATreeItem(const AATreeItem&) = delete; + AATreeItem& operator=(const AATreeItem&) = delete; + + protected: + constexpr explicit AATreeItem() = default; + + private: + // Allow the tree and its iterators to walk and maniplate the node-related + // fields of items. + friend class GenericAATree; + + template + friend class AATree; + + template + friend class AATreeIterator; + + /// Gets the level of an item, i.e. its depth in the tree. + uint8_t GetLevel() const; + + /// Sets the level of an item, i.e. its depth in the tree. + void SetLevel(uint8_t level); + + /// Returns whether this node is part of any tree. + [[nodiscard]] bool IsMapped() const; + + /// Returns the number of items in the subtree rooted by this item, + /// including this one. + size_t GetTreeSize(); + + /// Returns the item at the root of the overall tree. + AATreeItem* GetRoot(); + + /// Returns the item in this item's subtree that is furthest to the left, or + /// null if this item is a leaf node. + AATreeItem* GetLeftmost(); + + /// Returns the item in this item's subtree that is furthest to the right, + /// or null if this item is a leaf node. + AATreeItem* GetRightmost(); + + /// Returns the rightmost item to the left of this item in its overall tree, + /// or null if this item is the tree's leftmost item. + AATreeItem* GetPredecessor(); + + /// Returns the leftmost item to the right of this item in its overall tree, + /// or null if this item is the tree's leftmost item. + AATreeItem* GetSuccessor(); + + /// Removes this item from its overall tree. + /// + /// This method creates a balanced subtree with the same items as this + /// item's subtree, except for the item itself. + /// + /// Calling this method on an item that is not part of a tree has no effect. + /// + /// @returns An item with a balanced subtree. + AATreeItem* Unmap(); + + /// Sets the left child of this item, and sets that child's parent to this + /// item. + void SetLeft(AATreeItem* left); + + /// Sets the right child of this item, and sets that child's parent to this + /// item. + void SetRight(AATreeItem* right); + + /// Sets either the left or right child of this node to `new_child` if it + /// is currently `old_child`. + void Replace(AATreeItem* old_child, AATreeItem* new_child); + + /// Performs a right rotation on this item's subtree, if necessary. This + /// removes horizontal left links, where a horizontal link is between two + /// items of the same level. + /// + /// @returns A subtree with left horizontal links removed. + AATreeItem* Skew(); + + /// Performs a left rotation on this item's subtree, if necessary. This + /// removes consectuive horizontal right links, where a horizontal link is + /// between two items of the same level. + /// + /// @returns A subtree with consecutive right horizontal links removed. + AATreeItem* Split(); + + /// Walks up the overall tree from this node, skewing and splitting as + /// needed to produce a balanced AA tree. + /// + /// @returns An item representing the root of a balanced AA tree. + AATreeItem* Rebalance(); + + /// Removes the current item and all the items in its subtree from its + /// overall tree. + void Clear(); + + /// Restores all the fields of this item except its key to their default + /// values. Has no effect on any other item. + void Reset(); + + PackedPtr parent_; + mutable PackedPtr left_; + mutable PackedPtr right_; +}; + +/// Functor that gets a key from an item with a dedicated accessor. +template +struct GetKey { + const Key& operator()(const AATreeItem& item) const { + return static_cast(item).key(); + } +}; + +} // namespace pw::containers::internal diff --git a/pw_containers/public/pw_containers/internal/aa_tree_iterator.h b/pw_containers/public/pw_containers/internal/aa_tree_iterator.h new file mode 100644 index 0000000000..bf9fa07949 --- /dev/null +++ b/pw_containers/public/pw_containers/internal/aa_tree_iterator.h @@ -0,0 +1,127 @@ +// Copyright 2024 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +#pragma once + +#include +#include +#include + +#include "pw_containers/internal/aa_tree_item.h" + +namespace pw::containers::internal { + +/// Iterator that has the ability to advance forwards or backwards over a +/// sequence of items. +/// +/// This is roughly equivalent to std::bidirectional_iterator, but for +/// intrusive maps. +template +class AATreeIterator { + public: + using difference_type = std::ptrdiff_t; + using value_type = std::remove_cv_t; + using pointer = T*; + using reference = T&; + using iterator_category = std::bidirectional_iterator_tag; + + // constexpr AATreeIterator() = default; + + constexpr AATreeIterator(const AATreeIterator& other) { + *this = other; + } + + constexpr AATreeIterator& operator=(const AATreeIterator& other) { + root_ = other.root_; + item_ = other.item_; + return *this; + } + + constexpr const T& operator*() const { return *(downcast()); } + constexpr T& operator*() { return *(downcast()); } + + constexpr const T* operator->() const { return downcast(); } + constexpr T* operator->() { return downcast(); } + + template , std::remove_cv_t>>> + constexpr bool operator==(const AATreeIterator& rhs) const { + return root_ == rhs.root_ && item_ == rhs.item_; + } + + template , std::remove_cv_t>>> + constexpr bool operator!=(const AATreeIterator& rhs) const { + return !operator==(rhs); + } + + constexpr AATreeIterator& operator++() { + if (item_ != nullptr) { + item_ = item_->GetSuccessor(); + } else { + PW_DASSERT(root_ != nullptr); + PW_DASSERT(*root_ != nullptr); + item_ = (*root_)->GetLeftmost(); + } + return *this; + } + + constexpr AATreeIterator operator++(int) { + AATreeIterator copy = *this; + operator++(); + return copy; + } + + constexpr AATreeIterator& operator--() { + if (item_ != nullptr) { + item_ = item_->GetPredecessor(); + } else { + PW_DASSERT(root_ != nullptr); + PW_DASSERT(*root_ != nullptr); + item_ = (*root_)->GetRightmost(); + } + return *this; + } + + constexpr AATreeIterator operator--(int) { + AATreeIterator copy = *this; + operator--(); + return copy; + } + + private: + template + friend class AATreeIterator; + + friend class GenericAATree; + + template + friend class AATree; + + // Only the generic and derived AA trees can create iterators that actually + // point to something. They provides a pointer to its pointer to the root of + // tree. The root item may change, but the pointer to it remains the same for + // the life of the tree. + constexpr explicit AATreeIterator(AATreeItem** root) : root_(root) {} + constexpr AATreeIterator(AATreeItem** root, AATreeItem* item) + : root_(root), item_(item) {} + + T* downcast() { return static_cast(item_); } + + AATreeItem** root_; + mutable AATreeItem* item_ = nullptr; +}; + +} // namespace pw::containers::internal diff --git a/pw_containers/public/pw_containers/internal/intrusive_list.h b/pw_containers/public/pw_containers/internal/intrusive_list.h index 96e71a37c8..6705809a09 100644 --- a/pw_containers/public/pw_containers/internal/intrusive_list.h +++ b/pw_containers/public/pw_containers/internal/intrusive_list.h @@ -17,6 +17,7 @@ #include #include +#include "pw_containers/internal/intrusive.h" #include "pw_containers/internal/intrusive_list_item.h" namespace pw::containers::internal { @@ -76,8 +77,7 @@ class GenericIntrusiveList { /// Returns how many items can be added. /// - /// As an intrusive list, this is effectively unbounded, but is provided to - /// match std::list. + /// As an intrusive container, this is effectively unbounded. constexpr size_t max_size() const noexcept { return std::numeric_limits::max(); } diff --git a/pw_containers/public/pw_containers/intrusive_map.h b/pw_containers/public/pw_containers/intrusive_map.h new file mode 100644 index 0000000000..415ec03d04 --- /dev/null +++ b/pw_containers/public/pw_containers/intrusive_map.h @@ -0,0 +1,304 @@ +// Copyright 2024 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +#pragma once + +#include + +#include "pw_containers/internal/aa_tree.h" +#include "pw_containers/internal/aa_tree_item.h" +#include "pw_containers/internal/intrusive.h" + +namespace pw { + +// Forward declaration for friending. +template +class IntrusiveMultiMap; + +/// A `std::map`-like class that uses intrusive items. +/// +/// Since the map structure is stored in the items themselves, each item must +/// outlive any map it is a part of and must be part of at most one map. +/// +/// This map requires unique keys. Attempting to add an item with same key as an +/// item already in the map will fail. +/// +/// Do not use this base class directly. Use one of the specialization of +/// `pw::IntrsuiveMap` instead. +/// +/// * Since items are not allocated by this class, the following methods have +/// no analogue: +/// std::map::operator= +/// std::map::operator[] +/// std::map::get_allocator +/// std::map::insert_or_assign +/// std::map::emplace +/// std::map::emplace_hint +/// std::map::try_emplace +/// +/// * Methods corresponding to the following take initializer lists of pointer +/// to items rather than the itenms themselves: +/// std::map::(constructor) +/// std::map::insert +/// +/// * There are no overloads corresponding to the following methods that take +/// r-value references.: +/// std::map::insert +/// std::map::merge +/// +/// * Since modifying the map modifies the items themselves, methods +/// corresponding to those below only take `iterator`s and not +/// `const_iterator`s: +/// std::map::insert +/// std::map::erase +/// +/// * C++23 methods are not (yet) supported. +/// +/// @tparam Key Type to sort items on +/// @tparam T Type of values stored in the map. +/// @tparam Compare Function with the signature `bool(Key, Key) that is +/// used to order items. +/// @tparam GetKey Function with signature `Key(const T&)` that +/// returns the value that items are sorted on. +template , + typename GetKey = containers::internal::GetKey> +class IntrusiveMap { + private: + using ItemBase = containers::internal::AATreeItem; + using GenericIterator = containers::internal::GenericAATree::iterator; + using Tree = containers::internal::AATree; + + public: + class Item : public ItemBase { + public: + constexpr explicit Item() = default; + + private: + // GetElementTypeFromItem is used to find the element type from an item. + // It is used to ensure list items inherit from the correct Item type. + template + friend struct containers::internal::GetElementTypeFromItem; + using ElementType = T; + }; + + using key_type = typename Tree::Key; + using mapped_type = std::remove_cv_t; + using value_type = Item; + using size_type = std::size_t; + using reference = value_type&; + using const_reference = const value_type&; + using pointer = value_type*; + using const_pointer = const value_type*; + using key_compare = Compare; + + public: + class iterator : public containers::internal::AATreeIterator { + public: + constexpr iterator() = default; + + private: + using Base = containers::internal::AATreeIterator; + friend IntrusiveMap; + constexpr explicit iterator(GenericIterator iter) : Base(iter) {} + }; + + class const_iterator + : public containers::internal::AATreeIterator> { + public: + constexpr const_iterator() = default; + + private: + using Base = containers::internal::AATreeIterator>; + friend IntrusiveMap; + constexpr explicit const_iterator(GenericIterator iter) : Base(iter) {} + }; + + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + /// Constructs an empty map. + constexpr IntrusiveMap() : tree_(true) { CheckItemType(); } + + /// Constructs an IntrusiveMap from an iterator over Items. + /// + /// The iterator may dereference as either Item& (e.g. from std::array) + /// or Item* (e.g. from std::initializer_list). + template + IntrusiveMap(Iterator first, Iterator last) : IntrusiveMap() { + tree_.insert(first, last); + } + + /// Constructs an IntrusiveMap from a std::initializer_list of pointers + /// to items. + IntrusiveMap(std::initializer_list items) + : IntrusiveMap(items.begin(), items.end()) {} + + // Element access + + /// Returns a reference to the item associated with the given key. + /// + /// @pre The map must contain an item associated with the key. + T& at(const key_type& key) { + auto iter = tree_.find(key); + PW_DASSERT(iter != tree_.end()); + return static_cast(*iter); + } + + const T& at(const key_type& key) const { + auto iter = tree_.find(key); + PW_DASSERT(iter != tree_.end()); + return static_cast(*iter); + } + + // Iterators + + iterator begin() noexcept { return iterator(tree_.begin()); } + const_iterator begin() const noexcept { + return const_iterator(tree_.begin()); + } + const_iterator cbegin() const noexcept { return begin(); } + + iterator end() noexcept { return iterator(tree_.end()); } + const_iterator end() const noexcept { return const_iterator(tree_.end()); } + const_iterator cend() const noexcept { return end(); } + + reverse_iterator rbegin() noexcept { return reverse_iterator(end()); } + const_reverse_iterator rbegin() const noexcept { + return const_reverse_iterator(end()); + } + const_reverse_iterator crbegin() const noexcept { return rbegin(); } + + reverse_iterator rend() noexcept { return reverse_iterator(begin()); } + const_reverse_iterator rend() const noexcept { + return const_reverse_iterator(begin()); + } + const_reverse_iterator crend() const noexcept { return rend(); } + + // Capacity + + /// @copydoc GenericAATree::empty + [[nodiscard]] bool empty() const noexcept { return tree_.empty(); } + + /// @copydoc GenericAATree::size + size_t size() const { return tree_.size(); } + + /// @copydoc GenericAATree::max_size + constexpr size_t max_size() const noexcept { return tree_.max_size(); } + + // Modifiers + + /// @copydoc GenericAATree::clear + void clear() { tree_.clear(); } + + /// @copydoc AATree::insert + std::pair insert(T& item) { + auto result = tree_.insert(item); + return std::make_pair(iterator(result.first), result.second); + } + + iterator insert(iterator, T& item) { + // Disregard the hint. + return insert(item).first; + } + + template + void insert(Iterator first, Iterator last) { + tree_.insert(first, last); + } + + void insert(std::initializer_list ilist) { + tree_.insert(ilist.begin(), ilist.end()); + } + + /// @copydoc GenericAATree::clear + iterator erase(iterator pos) { return iterator(tree_.erase(*pos)); } + + iterator erase(iterator first, iterator last) { + return iterator(tree_.erase(*first, *last)); + } + + size_t erase(const key_type& key) { return tree_.erase(key); } + + /// @copydoc GenericAATree::swap + void swap(IntrusiveMap& other) { + tree_.swap(other.tree_); + } + + /// @copydoc AATree::merge + template + void merge(MapType& other) { + tree_.merge(other.tree_); + } + + /// @copydoc AATree::count + size_t count(const key_type& key) const { return tree_.count(key); } + + /// @copydoc AATree::find + iterator find(const key_type& key) { return iterator(tree_.find(key)); } + + const_iterator find(const key_type& key) const { + return const_iterator(tree_.find(key)); + } + + /// @copydoc AATree::equal_range + std::pair equal_range(const key_type& key) { + auto result = tree_.equal_range(key); + return std::make_pair(iterator(result.first), iterator(result.second)); + } + std::pair equal_range( + const key_type& key) const { + auto result = tree_.equal_range(key); + return std::make_pair(const_iterator(result.first), + const_iterator(result.second)); + } + + /// @copydoc AATree::lower_bound + iterator lower_bound(const key_type& key) { + return iterator(tree_.lower_bound(key)); + } + const_iterator lower_bound(const key_type& key) const { + return const_iterator(tree_.lower_bound(key)); + } + + /// @copydoc AATree::upper_bound + iterator upper_bound(const key_type& key) { + return iterator(tree_.upper_bound(key)); + } + const_iterator upper_bound(const key_type& key) const { + return const_iterator(tree_.upper_bound(key)); + } + + private: + // Check that T is an Item in a function, since the class T will not be fully + // defined when the IntrusiveList class is instantiated. + static constexpr void CheckItemType() { + using Base = ::pw::containers::internal::ElementTypeFromItem; + static_assert( + std::is_base_of(), + "IntrusiveMap items must be derived from IntrusiveMap::Item, " + "where T is the item or one of its bases."); + } + + // Allow multimaps to access the tree for `merge`. + template + friend class IntrusiveMultiMap; + + // The AA tree that stores the map. + // + // This field is mutable so that it doesn't need const overloads. + mutable Tree tree_; +}; + +} // namespace pw diff --git a/pw_containers/public/pw_containers/intrusive_multimap.h b/pw_containers/public/pw_containers/intrusive_multimap.h new file mode 100644 index 0000000000..1e164ed29c --- /dev/null +++ b/pw_containers/public/pw_containers/intrusive_multimap.h @@ -0,0 +1,278 @@ +// Copyright 2024 The Pigweed Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +#pragma once + +#include "pw_containers/internal/aa_tree.h" +#include "pw_containers/internal/aa_tree_item.h" +#include "pw_containers/internal/intrusive.h" + +namespace pw { + +// Forward declaration for friending. +template +class IntrusiveMap; + +/// A `std::multimap`-like class that uses intrusive items. +/// +/// Since the map structure is stored in the items themselves, each item must +/// outlive any map it is a part of and must be part of at most one map. +/// +/// This map requires unique keys. Attempting to add an item with same key as an +/// item already in the map will fail. +/// +/// Do not use this base class directly. Use one of the specialization of +/// `pw::IntrsuiveMap` instead. +/// +/// * Since items are not allocated by this class, the following methods have +/// no analogue: +/// std::multimap::operator= +/// std::multimap::get_allocator +/// std::multimap::emplace +/// std::multimap::emplace_hint +/// +/// * Methods corresponding to the following take initializer lists of pointer +/// to items rather than the itenms themselves: +/// std::multimap::(constructor) +/// std::multimap::insert +/// +/// * There are no overloads corresponding to the following methods that take +/// r-value references.: +/// std::multimap::insert +/// std::multimap::merge +/// +/// * Since modifying the map modifies the items themselves, methods +/// corresponding to those below only take `iterator`s and not +/// `const_iterator`s: +/// std::multimap::insert +/// std::multimap::erase +/// +/// * C++23 methods are not (yet) supported. +/// +/// @tparam Key Type to sort items on +/// @tparam T Type of values stored in the map. +/// @tparam GetKey Function with signature `Key(const T&)` that +/// returns the value that items are sorted on. +/// @tparam Compare Function with the signature `bool(Key, Key) that is +/// used to order items. +template , + typename GetKey = containers::internal::GetKey> +class IntrusiveMultiMap { + private: + using ItemBase = containers::internal::AATreeItem; + using GenericIterator = containers::internal::GenericAATree::iterator; + using Tree = containers::internal::AATree; + + public: + class Item : public ItemBase { + public: + constexpr explicit Item() = default; + + private: + // GetElementTypeFromItem is used to find the element type from an item. + // It is used to ensure list items inherit from the correct Item type. + template + friend struct containers::internal::GetElementTypeFromItem; + using ElementType = T; + }; + + using key_type = typename Tree::Key; + using mapped_type = std::remove_cv_t; + using value_type = Item; + using size_type = std::size_t; + using reference = value_type&; + using const_reference = const value_type&; + using pointer = value_type*; + using const_pointer = const value_type*; + using key_compare = Compare; + + public: + class iterator : public containers::internal::AATreeIterator { + public: + constexpr iterator() = default; + + private: + using Base = containers::internal::AATreeIterator; + friend IntrusiveMultiMap; + constexpr explicit iterator(GenericIterator iter) : Base(iter) {} + }; + + class const_iterator + : public containers::internal::AATreeIterator> { + public: + constexpr const_iterator() = default; + + private: + using Base = containers::internal::AATreeIterator>; + friend IntrusiveMultiMap; + constexpr explicit const_iterator(GenericIterator iter) : Base(iter) {} + }; + + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + /// Constructs an empty multimap. + constexpr IntrusiveMultiMap() : tree_(false) { CheckItemType(); } + + /// Constructs an IntrusiveMultiMap from an iterator over Items. + /// + /// The iterator may dereference as either Item& (e.g. from std::array) + /// or Item* (e.g. from std::initializer_list). + template + IntrusiveMultiMap(Iterator first, Iterator last) : IntrusiveMultiMap() { + tree_.insert(first, last); + } + + /// Constructs an IntrusiveMultiMap from a std::initializer_list of pointers + /// to items. + IntrusiveMultiMap(std::initializer_list items) + : IntrusiveMultiMap(items.begin(), items.end()) {} + + // Iterators + + iterator begin() noexcept { return iterator(tree_.begin()); } + const_iterator begin() const noexcept { + return const_iterator(tree_.begin()); + } + const_iterator cbegin() const noexcept { return begin(); } + + iterator end() noexcept { return iterator(tree_.end()); } + const_iterator end() const noexcept { return const_iterator(tree_.end()); } + const_iterator cend() const noexcept { return end(); } + + reverse_iterator rbegin() noexcept { return reverse_iterator(end()); } + const_reverse_iterator rbegin() const noexcept { + return const_reverse_iterator(end()); + } + const_reverse_iterator crbegin() const noexcept { return rbegin(); } + + reverse_iterator rend() noexcept { return reverse_iterator(begin()); } + const_reverse_iterator rend() const noexcept { + return const_reverse_iterator(begin()); + } + const_reverse_iterator crend() const noexcept { return rend(); } + + // Capacity + + /// @copydoc GenericAATree::empty + [[nodiscard]] bool empty() const noexcept { return tree_.empty(); } + + /// @copydoc GenericAATree::size + size_t size() const { return tree_.size(); } + + /// @copydoc GenericAATree::max_size + constexpr size_t max_size() const noexcept { return tree_.max_size(); } + + // Modifiers + + /// @copydoc GenericAATree::clear + void clear() { tree_.clear(); } + + /// @copydoc AATree::insert + iterator insert(T& item) { return iterator(tree_.insert(item).first); } + iterator insert(iterator, T& item) { + // Disregard the hint. + return iterator(insert(item)); + } + + template + void insert(Iterator first, Iterator last) { + tree_.insert(first, last); + } + + void insert(std::initializer_list ilist) { + tree_.insert(ilist.begin(), ilist.end()); + } + + /// @copydoc GenericAATree::clear + iterator erase(iterator pos) { return iterator(tree_.erase(*pos)); } + + iterator erase(iterator first, iterator last) { + return iterator(tree_.erase(*first, *last)); + } + + size_t erase(const Key& key) { return tree_.erase(key); } + + /// @copydoc GenericAATree::swap + void swap(IntrusiveMultiMap& other) { + tree_.swap(other.tree_); + } + + /// @copydoc AATree::merge + template + void merge(MapType& other) { + tree_.merge(other.tree_); + } + + /// @copydoc AATree::count + size_t count(const Key& key) const { return tree_.count(key); } + + /// @copydoc AATree::find + iterator find(const Key& key) { return iterator(tree_.find(key)); } + + const_iterator find(const Key& key) const { + return const_iterator(tree_.find(key)); + } + + /// @copydoc AATree::equal_range + std::pair equal_range(const Key& key) { + auto result = tree_.equal_range(key); + return std::make_pair(iterator(result.first), iterator(result.second)); + } + std::pair equal_range(const Key& key) const { + auto result = tree_.equal_range(key); + return std::make_pair(const_iterator(result.first), + const_iterator(result.second)); + } + + /// @copydoc AATree::lower_bound + iterator lower_bound(const Key& key) { + return iterator(tree_.lower_bound(key)); + } + const_iterator lower_bound(const Key& key) const { + return const_iterator(tree_.lower_bound(key)); + } + + /// @copydoc AATree::upper_bound + iterator upper_bound(const Key& key) { + return iterator(tree_.upper_bound(key)); + } + const_iterator upper_bound(const Key& key) const { + return const_iterator(tree_.upper_bound(key)); + } + + private: + // Check that T is an Item in a function, since the class T will not be fully + // defined when the IntrusiveList class is instantiated. + static constexpr void CheckItemType() { + using Base = ::pw::containers::internal::ElementTypeFromItem; + static_assert( + std::is_base_of(), + "IntrusiveMultiMap items must be derived from " + "IntrusiveMultiMap::Item, where T is the item or one of its " + "bases."); + } + + // Allow maps to access the tree for `merge`. + template + friend class IntrusiveMap; + + // The AA tree that stores the map. + // + // This field is mutable so that it doesn't need const overloads. + mutable Tree tree_; +}; + +} // namespace pw