diff --git a/framework/global/tests/CMakeLists.txt b/framework/global/tests/CMakeLists.txt
index f69ce1a324..00e1747c0c 100644
--- a/framework/global/tests/CMakeLists.txt
+++ b/framework/global/tests/CMakeLists.txt
@@ -57,6 +57,8 @@ set(MODULE_TEST_SRC
${CMAKE_CURRENT_LIST_DIR}/ringqueue_tests.cpp
${CMAKE_CURRENT_LIST_DIR}/rpcqueue_tests.cpp
${CMAKE_CURRENT_LIST_DIR}/geometry_tests.cpp
+ ${CMAKE_CURRENT_LIST_DIR}/sharedmap_tests.cpp
+ ${CMAKE_CURRENT_LIST_DIR}/sharedhashmap_tests.cpp
)
include(SetupGTest)
diff --git a/framework/global/tests/sharedhashmap_tests.cpp b/framework/global/tests/sharedhashmap_tests.cpp
new file mode 100644
index 0000000000..eea2a0efe9
--- /dev/null
+++ b/framework/global/tests/sharedhashmap_tests.cpp
@@ -0,0 +1,209 @@
+/*
+ * SPDX-License-Identifier: GPL-3.0-only
+ * MuseScore-CLA-applies
+ *
+ * MuseScore Studio
+ * Music Composition & Notation
+ *
+ * Copyright (C) 2026 MuseScore Limited and others
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+#include
+
+#include
+#include
+
+#include "types/sharedhashmap.h"
+
+using namespace muse;
+
+class Global_Types_SharedHashMapTests : public ::testing::Test
+{
+public:
+};
+
+TEST_F(Global_Types_SharedHashMapTests, ErasePos_WhileShared)
+{
+ // [GIVEN]
+ SharedHashMap original { { 1, "a" }, { 2, "b" }, { 3, "c" } };
+ SharedHashMap copy = original;
+
+ // [WHEN]
+ auto it = std::as_const(copy).find(2);
+ copy.erase(it);
+
+ // [THEN]
+ EXPECT_EQ(copy.size(), 2);
+ EXPECT_FALSE(copy.contains(2));
+ EXPECT_EQ(original.size(), 3);
+ EXPECT_TRUE(original.contains(2));
+}
+
+TEST_F(Global_Types_SharedHashMapTests, EraseRange_Full_WhileShared)
+{
+ // [GIVEN]
+ SharedHashMap original { { 1, "a" }, { 2, "b" } };
+ SharedHashMap copy = original;
+
+ // [WHEN]
+ copy.erase(copy.cbegin(), copy.cend());
+
+ // [THEN]
+ EXPECT_TRUE(copy.empty());
+ EXPECT_EQ(original.size(), 2);
+}
+
+TEST_F(Global_Types_SharedHashMapTests, EraseRange_Partial_WhileShared)
+{
+ // [GIVEN]
+ SharedHashMap original { { 1, "a" }, { 2, "b" }, { 3, "c" }, { 4, "d" } };
+ SharedHashMap copy = original;
+
+ // [WHEN]
+ auto first = std::as_const(copy).find(2);
+ auto last = std::next(first);
+ copy.erase(first, last);
+
+ // [THEN]
+ EXPECT_EQ(copy.size(), 3);
+ EXPECT_FALSE(copy.contains(2));
+ EXPECT_EQ(original.size(), 4);
+ EXPECT_TRUE(original.contains(2));
+}
+
+TEST_F(Global_Types_SharedHashMapTests, EraseRange_Empty_WhileShared_IsNoOp)
+{
+ // [GIVEN]
+ SharedHashMap original { { 1, "a" }, { 2, "b" } };
+ SharedHashMap copy = original;
+
+ // [WHEN]
+ auto it = std::as_const(copy).find(1);
+ copy.erase(it, it);
+
+ // [THEN]
+ EXPECT_EQ(copy.size(), 2);
+ EXPECT_EQ(original.size(), 2);
+}
+
+TEST_F(Global_Types_SharedHashMapTests, InsertHint_AtEnd_WhileShared)
+{
+ // [GIVEN]
+ SharedHashMap original { { 1, "a" } };
+ SharedHashMap copy = original;
+
+ // [WHEN]
+ copy.insert(copy.cend(), { 2, "b" });
+
+ // [THEN]
+ EXPECT_EQ(copy.size(), 2);
+ EXPECT_TRUE(copy.contains(2));
+ EXPECT_EQ(original.size(), 1);
+ EXPECT_FALSE(original.contains(2));
+}
+
+TEST_F(Global_Types_SharedHashMapTests, InsertHint_AtExistingElement_WhileShared)
+{
+ // [GIVEN]
+ SharedHashMap original { { 1, "a" } };
+ SharedHashMap copy = original;
+
+ // [WHEN]
+ copy.insert(std::as_const(copy).find(1), { 2, "b" });
+
+ // [THEN]
+ EXPECT_EQ(copy.size(), 2);
+ EXPECT_TRUE(copy.contains(2));
+ EXPECT_EQ(original.size(), 1);
+ EXPECT_FALSE(original.contains(2));
+}
+
+TEST_F(Global_Types_SharedHashMapTests, InsertHint_IntoEmptyMap_WhileShared)
+{
+ // [GIVEN]
+ SharedHashMap original;
+ SharedHashMap copy = original;
+
+ // [WHEN] cbegin() == cend() on an empty map
+ ASSERT_EQ(copy.cbegin(), copy.cend());
+ copy.insert(copy.cend(), { 1, "a" });
+
+ // [THEN]
+ EXPECT_EQ(copy.size(), 1);
+ EXPECT_TRUE(original.empty());
+}
+
+TEST_F(Global_Types_SharedHashMapTests, EmplaceHint_WhileShared)
+{
+ // [GIVEN]
+ SharedHashMap original { { 1, "a" } };
+ SharedHashMap copy = original;
+
+ // [WHEN]
+ copy.emplace_hint(copy.cend(), 2, "b");
+
+ // [THEN]
+ EXPECT_EQ(copy.size(), 2);
+ EXPECT_TRUE(copy.contains(2));
+ EXPECT_EQ(original.size(), 1);
+}
+
+TEST_F(Global_Types_SharedHashMapTests, TryEmplaceHint_NewKey_WhileShared)
+{
+ // [GIVEN]
+ SharedHashMap original { { 1, "a" } };
+ SharedHashMap copy = original;
+
+ // [WHEN]
+ copy.try_emplace(copy.cend(), 2, "b");
+
+ // [THEN]
+ EXPECT_EQ(copy.size(), 2);
+ EXPECT_EQ(copy.at(2), "b");
+ EXPECT_EQ(original.size(), 1);
+}
+
+TEST_F(Global_Types_SharedHashMapTests, TryEmplaceHint_ExistingKey_WhileShared_DoesNotOverwrite)
+{
+ // [GIVEN]
+ SharedHashMap original { { 1, "a" } };
+ SharedHashMap copy = original;
+
+ // [WHEN]
+ copy.try_emplace(std::as_const(copy).find(1), 1, "z");
+
+ // [THEN]
+ EXPECT_EQ(copy.size(), 1);
+ EXPECT_EQ(copy.at(1), "a");
+}
+
+TEST_F(Global_Types_SharedHashMapTests, Merge_WithMapSharingStorage)
+{
+ // [GIVEN]
+ SharedHashMap a { { 1, "a" }, { 2, "b" } };
+ SharedHashMap b = a; // shares storage with a
+
+ // [WHEN]
+ a.insert({ 3, "c" }); // detaches a from b
+ a.merge(b);
+
+ // [THEN]
+ EXPECT_EQ(a.size(), 3);
+ EXPECT_TRUE(a.contains(1));
+ EXPECT_TRUE(a.contains(2));
+ EXPECT_TRUE(a.contains(3));
+ EXPECT_EQ(b.size(), 2);
+ EXPECT_TRUE(b.contains(1));
+ EXPECT_TRUE(b.contains(2));
+}
diff --git a/framework/global/tests/sharedmap_tests.cpp b/framework/global/tests/sharedmap_tests.cpp
new file mode 100644
index 0000000000..11fc42191f
--- /dev/null
+++ b/framework/global/tests/sharedmap_tests.cpp
@@ -0,0 +1,275 @@
+/*
+ * SPDX-License-Identifier: GPL-3.0-only
+ * MuseScore-CLA-applies
+ *
+ * MuseScore Studio
+ * Music Composition & Notation
+ *
+ * Copyright (C) 2026 MuseScore Limited and others
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+#include
+
+#include
+#include
+
+#include "types/sharedmap.h"
+
+using namespace muse;
+
+class Global_Types_SharedMapTests : public ::testing::Test
+{
+public:
+};
+
+TEST_F(Global_Types_SharedMapTests, ErasePos_Middle_WhileShared)
+{
+ // [GIVEN]
+ SharedMap original { { 1, "a" }, { 2, "b" }, { 3, "c" } };
+ SharedMap copy = original;
+
+ // [WHEN]
+ auto it = std::as_const(copy).find(2);
+ copy.erase(it);
+
+ // [THEN]
+ EXPECT_EQ(copy.size(), 2);
+ EXPECT_FALSE(copy.contains(2));
+ EXPECT_EQ(original.size(), 3);
+ EXPECT_TRUE(original.contains(2));
+}
+
+TEST_F(Global_Types_SharedMapTests, ErasePos_Begin_WhileShared)
+{
+ // [GIVEN]
+ SharedMap original { { 1, "a" }, { 2, "b" }, { 3, "c" } };
+ SharedMap copy = original;
+
+ // [WHEN]
+ copy.erase(copy.cbegin());
+
+ // [THEN]
+ EXPECT_EQ(copy.size(), 2);
+ EXPECT_FALSE(copy.contains(1));
+ EXPECT_EQ(original.size(), 3);
+ EXPECT_TRUE(original.contains(1));
+}
+
+TEST_F(Global_Types_SharedMapTests, EraseRange_Partial_WhileShared)
+{
+ // [GIVEN]
+ SharedMap original { { 1, "a" }, { 2, "b" }, { 3, "c" }, { 4, "d" } };
+ SharedMap copy = original;
+
+ // [WHEN]
+ copy.erase(std::as_const(copy).find(2), std::as_const(copy).find(4));
+
+ // [THEN]
+ EXPECT_EQ(copy.size(), 2);
+ EXPECT_TRUE(copy.contains(1));
+ EXPECT_FALSE(copy.contains(2));
+ EXPECT_FALSE(copy.contains(3));
+ EXPECT_TRUE(copy.contains(4));
+ EXPECT_EQ(original.size(), 4);
+}
+
+TEST_F(Global_Types_SharedMapTests, EraseRange_Full_WhileShared)
+{
+ // [GIVEN]
+ SharedMap original { { 1, "a" }, { 2, "b" } };
+ SharedMap copy = original;
+
+ // [WHEN]
+ copy.erase(copy.cbegin(), copy.cend());
+
+ // [THEN]
+ EXPECT_TRUE(copy.empty());
+ EXPECT_EQ(original.size(), 2);
+}
+
+TEST_F(Global_Types_SharedMapTests, EraseRange_BeginToKey_WhileShared)
+{
+ // [GIVEN]
+ SharedMap original { { 1, "a" }, { 2, "b" }, { 3, "c" } };
+ SharedMap copy = original;
+
+ // [WHEN] begin() paired with a key
+ copy.erase(copy.cbegin(), std::as_const(copy).find(3));
+
+ // [THEN]
+ EXPECT_EQ(copy.size(), 1);
+ EXPECT_FALSE(copy.contains(1));
+ EXPECT_FALSE(copy.contains(2));
+ EXPECT_TRUE(copy.contains(3));
+ EXPECT_EQ(original.size(), 3);
+}
+
+TEST_F(Global_Types_SharedMapTests, EraseRange_KeyToEnd_WhileShared)
+{
+ // [GIVEN]
+ SharedMap original { { 1, "a" }, { 2, "b" }, { 3, "c" } };
+ SharedMap copy = original;
+
+ // [WHEN] a key paired with end()
+ copy.erase(std::as_const(copy).find(2), copy.cend());
+
+ // [THEN]
+ EXPECT_EQ(copy.size(), 1);
+ EXPECT_TRUE(copy.contains(1));
+ EXPECT_FALSE(copy.contains(2));
+ EXPECT_FALSE(copy.contains(3));
+ EXPECT_EQ(original.size(), 3);
+}
+
+TEST_F(Global_Types_SharedMapTests, EraseRange_Empty_WhileShared_IsNoOp)
+{
+ // [GIVEN]
+ SharedMap original { { 1, "a" }, { 2, "b" }, { 3, "c" } };
+ SharedMap copy = original;
+
+ // [WHEN]
+ auto it = std::as_const(copy).find(2);
+ copy.erase(it, it);
+
+ // [THEN]
+ EXPECT_EQ(copy.size(), 3);
+ EXPECT_EQ(original.size(), 3);
+}
+
+TEST_F(Global_Types_SharedMapTests, InsertHint_AtEnd_WhileShared)
+{
+ // [GIVEN]
+ SharedMap original { { 1, "a" }, { 2, "b" } };
+ SharedMap copy = original;
+
+ // [WHEN]
+ copy.insert(copy.cend(), { 3, "c" });
+
+ // [THEN]
+ EXPECT_EQ(copy.size(), 3);
+ EXPECT_TRUE(copy.contains(3));
+ EXPECT_EQ(original.size(), 2);
+ EXPECT_FALSE(original.contains(3));
+}
+
+TEST_F(Global_Types_SharedMapTests, InsertHint_AtBegin_WhileShared)
+{
+ // [GIVEN]
+ SharedMap original { { 2, "b" }, { 3, "c" } };
+ SharedMap copy = original;
+
+ // [WHEN]
+ copy.insert(copy.cbegin(), { 1, "a" });
+
+ // [THEN]
+ EXPECT_EQ(copy.size(), 3);
+ EXPECT_TRUE(copy.contains(1));
+ EXPECT_EQ(original.size(), 2);
+ EXPECT_FALSE(original.contains(1));
+}
+
+TEST_F(Global_Types_SharedMapTests, InsertHint_Middle_WhileShared)
+{
+ // [GIVEN]
+ SharedMap original { { 1, "a" }, { 3, "c" } };
+ SharedMap copy = original;
+
+ // [WHEN]
+ copy.insert(std::as_const(copy).find(3), { 2, "b" });
+
+ // [THEN]
+ EXPECT_EQ(copy.size(), 3);
+ EXPECT_TRUE(copy.contains(2));
+ EXPECT_EQ(original.size(), 2);
+ EXPECT_FALSE(original.contains(2));
+}
+
+TEST_F(Global_Types_SharedMapTests, InsertHint_IntoEmptyMap_WhileShared)
+{
+ // [GIVEN]
+ SharedMap original;
+ SharedMap copy = original;
+
+ // [WHEN] cbegin() == cend() on an empty map
+ ASSERT_EQ(copy.cbegin(), copy.cend());
+ copy.insert(copy.cend(), { 1, "a" });
+
+ // [THEN]
+ EXPECT_EQ(copy.size(), 1);
+ EXPECT_TRUE(original.empty());
+}
+
+TEST_F(Global_Types_SharedMapTests, EmplaceHint_WhileShared)
+{
+ // [GIVEN]
+ SharedMap original { { 1, "a" } };
+ SharedMap copy = original;
+
+ // [WHEN]
+ copy.emplace_hint(copy.cend(), 2, "b");
+
+ // [THEN]
+ EXPECT_EQ(copy.size(), 2);
+ EXPECT_TRUE(copy.contains(2));
+ EXPECT_EQ(original.size(), 1);
+}
+
+TEST_F(Global_Types_SharedMapTests, TryEmplaceHint_NewKey_WhileShared)
+{
+ // [GIVEN]
+ SharedMap original { { 1, "a" } };
+ SharedMap copy = original;
+
+ // [WHEN]
+ copy.try_emplace(copy.cend(), 2, "b");
+
+ // [THEN]
+ EXPECT_EQ(copy.size(), 2);
+ EXPECT_EQ(copy.at(2), "b");
+ EXPECT_EQ(original.size(), 1);
+}
+
+TEST_F(Global_Types_SharedMapTests, TryEmplaceHint_ExistingKey_WhileShared_DoesNotOverwrite)
+{
+ // [GIVEN]
+ SharedMap original { { 1, "a" } };
+ SharedMap copy = original;
+
+ // [WHEN]
+ copy.try_emplace(copy.cbegin(), 1, "z");
+
+ // [THEN]
+ EXPECT_EQ(copy.size(), 1);
+ EXPECT_EQ(copy.at(1), "a");
+}
+
+TEST_F(Global_Types_SharedMapTests, Merge_WithMapSharingStorage)
+{
+ // [GIVEN]
+ SharedMap a { { 1, "a" }, { 2, "b" } };
+ SharedMap b = a; // shares storage with a
+
+ // [WHEN]
+ a.insert({ 3, "c" }); // detaches a from b
+ a.merge(b);
+
+ // [THEN]
+ EXPECT_EQ(a.size(), 3);
+ EXPECT_TRUE(a.contains(1));
+ EXPECT_TRUE(a.contains(2));
+ EXPECT_TRUE(a.contains(3));
+ EXPECT_EQ(b.size(), 2);
+ EXPECT_TRUE(b.contains(1));
+ EXPECT_TRUE(b.contains(2));
+}
diff --git a/framework/global/types/sharedhashmap.h b/framework/global/types/sharedhashmap.h
index 6858617a78..d72f1b787e 100644
--- a/framework/global/types/sharedhashmap.h
+++ b/framework/global/types/sharedhashmap.h
@@ -24,6 +24,7 @@
#include
#include
+#include
namespace muse {
template
@@ -138,18 +139,6 @@ class SharedHashMap
return find(key) != end();
}
- template
- bool containsAnyOf(BeginIt beginIt, EndIt endIt) const noexcept
- {
- for (auto it = beginIt; it != endIt; ++it) {
- if (this->contains(*it)) {
- return true;
- }
- }
-
- return false;
- }
-
bool empty() const noexcept
{
return m_dataPtr->empty();
@@ -172,12 +161,31 @@ class SharedHashMap
m_dataPtr->insert(std::forward(pair));
}
- void insert(iterator first, iterator last)
+ void insert(const_iterator first, const_iterator last)
{
ensureDetach();
m_dataPtr->insert(first, last);
}
+ iterator insert(const_iterator hint, const PairType& pair)
+ {
+ return withDetachedHint(hint, [this, &pair](const_iterator h) { return m_dataPtr->insert(h, pair); });
+ }
+
+ iterator insert(const_iterator hint, PairType&& pair)
+ {
+ return withDetachedHint(hint, [this, &pair](const_iterator h) {
+ return m_dataPtr->insert(h, std::forward(pair));
+ });
+ }
+
+ void merge(SharedHashMap& source)
+ {
+ ensureDetach();
+ source.ensureDetach();
+ m_dataPtr->merge(*source.m_dataPtr);
+ }
+
void insert_or_assign(const KeyType& key, ValType&& val)
{
ensureDetach();
@@ -197,6 +205,37 @@ class SharedHashMap
m_dataPtr->emplace(std::forward(args)...);
}
+ template
+ iterator emplace_hint(const_iterator hint, Args&& ... args)
+ {
+ return withDetachedHint(hint, [this, &args ...](const_iterator h) {
+ return m_dataPtr->emplace_hint(h, std::forward(args)...);
+ });
+ }
+
+ template
+ std::pair try_emplace(const KeyType& key, Args&& ... args)
+ {
+ ensureDetach();
+ return m_dataPtr->try_emplace(key, std::forward(args)...);
+ }
+
+ template
+ iterator try_emplace(const_iterator hint, const KeyType& key, Args&& ... args)
+ {
+ return withDetachedHint(hint, [this, &key, &args ...](const_iterator h) {
+ return m_dataPtr->try_emplace(h, key, std::forward(args)...);
+ });
+ }
+
+ template
+ iterator try_emplace(const_iterator hint, KeyType&& key, Args&& ... args)
+ {
+ return withDetachedHint(hint, [this, &key, &args ...](const_iterator h) {
+ return m_dataPtr->try_emplace(h, std::forward(key), std::forward(args)...);
+ });
+ }
+
void clear() noexcept
{
ensureDetach();
@@ -209,14 +248,27 @@ class SharedHashMap
m_dataPtr->erase(key);
}
- void erase(iterator first, iterator last)
+ iterator erase(const_iterator pos)
{
- ensureDetach();
- m_dataPtr->erase(first, last);
+ return withDetachedHint(pos, [this](const_iterator p) { return m_dataPtr->erase(p); });
+ }
+
+ void erase(const_iterator first, const_iterator last)
+ {
+ if (m_dataPtr.use_count() == 1) {
+ m_dataPtr->erase(first, last);
+ } else {
+ auto [dfirst, dlast] = detachAndReanchorRange(first, last);
+ m_dataPtr->erase(dfirst, dlast);
+ }
}
bool operator ==(const SharedHashMap& another) const noexcept
{
+ if (m_dataPtr == another.m_dataPtr) {
+ return true;
+ }
+
return *m_dataPtr == *another.m_dataPtr;
}
@@ -239,6 +291,54 @@ class SharedHashMap
m_dataPtr = std::make_shared(*m_dataPtr);
}
+ // Detach-safe descriptor of a position
+ struct AtEnd {};
+ using Anchor = std::variant;
+
+ Anchor describeAnchor(const_iterator it) const
+ {
+ if (it == m_dataPtr->cend()) {
+ return AtEnd {};
+ }
+ return it->first;
+ }
+
+ const_iterator resolveAnchor(const Anchor& a) const
+ {
+ if (std::holds_alternative(a)) {
+ return m_dataPtr->cend();
+ }
+ return m_dataPtr->find(std::get(a));
+ }
+
+ const_iterator detachAndReanchor(const_iterator hint)
+ {
+ const Anchor a = describeAnchor(hint);
+ ensureDetach();
+ return resolveAnchor(a);
+ }
+
+ template
+ auto withDetachedHint(const_iterator hint, Op&& op)
+ {
+ if (m_dataPtr.use_count() == 1) {
+ return op(hint);
+ }
+
+ const_iterator dhint = detachAndReanchor(hint);
+ return op(dhint);
+ }
+
+ std::pair detachAndReanchorRange(const_iterator first, const_iterator last)
+ {
+ const Anchor firstAnchor = describeAnchor(first);
+ const Anchor lastAnchor = describeAnchor(last);
+
+ ensureDetach();
+
+ return { resolveAnchor(firstAnchor), resolveAnchor(lastAnchor) };
+ }
+
DataPtr m_dataPtr = nullptr;
};
}
diff --git a/framework/global/types/sharedmap.h b/framework/global/types/sharedmap.h
index 71206cb32b..bc1f0469de 100644
--- a/framework/global/types/sharedmap.h
+++ b/framework/global/types/sharedmap.h
@@ -24,6 +24,7 @@
#include
#include