diff --git a/Code/max/Containers/SlotMap.hpp b/Code/max/Containers/SlotMap.hpp new file mode 100644 index 0000000..f9d929c --- /dev/null +++ b/Code/max/Containers/SlotMap.hpp @@ -0,0 +1,107 @@ +// Copyright 2021, The max Contributors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MAX_CONTAINERS_SLOTMAP_HPP +#define MAX_CONTAINERS_SLOTMAP_HPP + +#include +#include +#include +#include +#include + +#include +#include + + +namespace max +{ +MAX_CURRENT_VERSION_NAMESPACE_BEGIN(v0) +{ +namespace Containers +{ + + // SlotMap is a container that provides a stable handle to an element that was inserted. + // + // Insertion: O(1)* + // Access: O(1) + // Removal: O(1) + // *Unless the BackingType requires reallocation. Then it is O(1) amortized. + template> typename BackingType = std::vector> + class SlotMap { + public: + + + + // Required to satisfy the C++ "Container" requirements + typedef T value_type; + typedef T& reference; + typedef const T& const_reference; + typedef BackingType::iterator iterator; + typedef BackingType::const_iterator const_iterator; + typedef BackingType::difference_type difference_type; + typedef BackingType::size_type size_type; + + + + HandleType push_back(T element) noexcept; + + template + HandleType emplace_back(Args&&... args) noexcept; + + reference operator[](HandleType handle) noexcept; + const_reference operator[](HandleType handle) const noexcept; + + void remove(HandleType handle) noexcept; + void pop_back() noexcept; + + + + // Required to satisfy the C++ "Container" requirements + iterator begin() noexcept; + const_iterator begin() const noexcept; + const_iterator cbegin() const noexcept; + iterator end() noexcept; + const_iterator end() const noexcept; + const_iterator cend() noexcept; + + size_type size() const noexcept; + constexpr size_type max_size() const noexcept; + + + + private: + + // The indices are stable and act as handles. + BackingType indices_; + // If we wanted to prevent a user accidently reusing a handle from an object they removed, + // |indices_| could be a tuple of HandleType and generation counter. + // Increment the generation every time an element is removed and compare against it when accessing. + // However, this is designed for performance and will assume the programmer did not make a mistake. + + // |data_| and |reverse_indices_| are parallel. The nth element in one corresponds to the nth element in the other. + // This means once we have an element's index into |data_|, we can use that same index into |reverse_indices_| to find + // the element in |indices_| that points here. + BackingType data_; + BackingType reverse_indices_; + + void remove_by_index(size_t index) noexcept; + + template typename BackingType2> + friend bool operator ==(const SlotMap& lhs, const SlotMap& rhs) noexcept; + template typename BackingType2> + friend bool operator !=(const SlotMap& lhs, const SlotMap& rhs) noexcept; + template typename BackingType2> + friend void swap(SlotMap& lhs, SlotMap& rhs) noexcept; + + }; + +} // namespace Containers +} // MAX_CURRENT_VERSION_NAMESPACE_BEGIN( v0 ) +MAX_CURRENT_VERSION_NAMESPACE_END(v0) +} // namespace max + +#include "SlotMap.inl" + +#endif // #ifndef MAX_CONTAINERS_SLOTMAP_HPP \ No newline at end of file diff --git a/Code/max/Containers/SlotMap.inl b/Code/max/Containers/SlotMap.inl new file mode 100644 index 0000000..d7ac083 --- /dev/null +++ b/Code/max/Containers/SlotMap.inl @@ -0,0 +1,169 @@ +// Copyright 2021, The max Contributors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include +#include +#include + +namespace max +{ +namespace v0 +{ +namespace Containers +{ + + template typename BackingType> + HandleType SlotMap::push_back(T element) noexcept { + const auto data_size = data_.size(); + data_.push_back(std::move(element)); + + const auto reverse_indices_size = reverse_indices_.size(); + if (data_size == reverse_indices_size) { + // No elements had previously been removed, leaving gaps. + // This means we cannot reuse the handles and indices we left in place. + // We must add new ones. + auto new_handle = reverse_indices_size; + indices_.push_back(new_handle); + reverse_indices_.push_back(new_handle); + return new_handle; + } + else { + // We previously left the elements in |indices_| and |reverse_indices_|. + // That means we can just reuse those handles. + return reverse_indices_[data_size]; + } + } + + template typename BackingType> + template + HandleType SlotMap::emplace_back(Args&&... args) noexcept { + // TODO: constructing the element then moving it to push_back() really defeats the purpose of an emplace_back. + // Do this correctly. + return push_back(T{ std::forward(args)... }); + } + + template typename BackingType> + typename SlotMap::reference SlotMap::operator[](HandleType handle) noexcept { + // Assumes |handle| is within range. + + const auto index = indices_[handle]; + return data_[index]; + } + + template typename BackingType> + typename SlotMap::const_reference SlotMap::operator[](HandleType handle) const noexcept { + // Assumes |handle| is within range. + + const auto index = indices_[handle]; + return data_[index]; + } + + template typename BackingType> + void SlotMap::remove(HandleType handle) noexcept { + remove_by_index(indices_[handle]); + } + + template typename BackingType> + void SlotMap::pop_back() noexcept { + remove_by_index(data_.size() - 1); + } + + + + // Required to satisfy the C++ "Container" requirements + template typename BackingType> + typename SlotMap::iterator SlotMap::begin() noexcept { + return data_.begin(); + } + + template typename BackingType> + typename SlotMap::const_iterator SlotMap::begin() const noexcept { + return data_.begin(); + } + + template typename BackingType> + typename SlotMap::const_iterator SlotMap::cbegin() const noexcept { + return data_.begin(); + } + + template typename BackingType> + typename SlotMap::iterator SlotMap::end() noexcept { + return data_.end(); + } + + template typename BackingType> + typename SlotMap::const_iterator SlotMap::end() const noexcept { + return data_.end(); + } + + template typename BackingType> + typename SlotMap::const_iterator SlotMap::cend() noexcept { + return data_.end(); + } + + template typename BackingType> + typename SlotMap::size_type SlotMap::size() const noexcept { + return data_.size(); + } + + template typename BackingType> + constexpr typename SlotMap::size_type SlotMap::max_size() const noexcept { + return data_.max_size(); + } + + template typename BackingType> + void SlotMap::remove_by_index(size_t index) noexcept { + // Removing an element in the middle of a vector will cause all elements after it to shift over. + // Removing the final element does not have this effect. + + // In order to remove any given element, we first swap it with the element in the final spot. + // Then we can remove it and update indices. + + using std::swap; + + auto data_size = data_.size() - 1; // .size() tells us the index past the end. + + swap(data_[index], data_[data_size]); + swap(reverse_indices_[index], reverse_indices_[data_size]); // We only resize |data_|, so use it's size--not |reverse_indices_|'s. + data_.resize(data_size); + + // The catch is we only know one index, the parameter. + // We don't know which element in |indices_| corresponds to the final element in |data_|. + // To solve that problem, the |reverse_indices_| vector is parallel to the |data_| vector. + // An index into one is also an index into the other. + // We have the index of the final element in |data_|, so we can use it in |reverse_indices_| to get the corresponding entry in |indices_|. + auto reverse_index = reverse_indices_[data_size]; + indices_[reverse_index] = data_size; + + // We also update the reverse index of the that was removed. + // This is because it will be reused later when more elements are appended. + reverse_index = reverse_indices_[index]; + indices_[reverse_index] = index; + } + + template typename BackingType> + bool operator ==(const SlotMap& lhs, const SlotMap& rhs) noexcept { + return lhs.indices_ == rhs.indices_ && + lhs.data_ == rhs.data_ && + lhs.reverse_indices_ == rhs.reverse_indices_; + } + + template typename BackingType> + bool operator !=(const SlotMap& lhs, const SlotMap& rhs) noexcept { + return !(lhs == rhs); + } + + template typename BackingType> + void swap(SlotMap& lhs, SlotMap& rhs) noexcept { + using std::swap; + + swap(lhs.indices_, rhs.indices_); + swap(lhs.data_, rhs.data_); + swap(lhs.reverse_indices_, rhs.reverse_indices_); + } + +} // namespace Containers +} // namespace v0 +} // namespace max diff --git a/Code/max/Containers/SlotMapTest.cpp b/Code/max/Containers/SlotMapTest.cpp new file mode 100644 index 0000000..4909598 --- /dev/null +++ b/Code/max/Containers/SlotMapTest.cpp @@ -0,0 +1,189 @@ +// Copyright 2026, The max Contributors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "SlotMapTest.hpp" +#include +#include +#include +#include + +namespace maxAutomatedTests +{ +namespace Containers +{ + + void RunSlotMapTestSuite() + { + max::Testing::CoutResultPolicy ResultPolicy; + auto SlotMapTestSuite = max::Testing::TestSuite< max::Testing::CoutResultPolicy >{ "max::Containers::SlotMap test suite", std::move(ResultPolicy) }; + + SlotMapTestSuite.AddTest(max::Testing::Test< max::Testing::CoutResultPolicy >{ "push_back appends element", [](max::Testing::Test< max::Testing::CoutResultPolicy >& CurrentTest, max::Testing::CoutResultPolicy const& ResultPolicy) { + auto test = max::Containers::SlotMap< int >{}; + CurrentTest.MAX_TESTING_ASSERT(test.size() == 0); + + auto handle = test.push_back(10); + + CurrentTest.MAX_TESTING_ASSERT(test.size() == 1); + CurrentTest.MAX_TESTING_ASSERT(test[handle] == 10); + } + }); + + SlotMapTestSuite.AddTest(max::Testing::Test< max::Testing::CoutResultPolicy >{ "emplace_back constructs in-place, appended", [](max::Testing::Test< max::Testing::CoutResultPolicy >& CurrentTest, max::Testing::CoutResultPolicy const& ResultPolicy) { + struct Element { + int first_; + int second_; + }; + auto test = max::Containers::SlotMap< Element >{}; + CurrentTest.MAX_TESTING_ASSERT(test.size() == 0); + + auto handle = test.emplace_back(10, 20); + + CurrentTest.MAX_TESTING_ASSERT(test.size() == 1); + CurrentTest.MAX_TESTING_ASSERT(test[handle].first_ == 10); + CurrentTest.MAX_TESTING_ASSERT(test[handle].second_ == 20); + } + }); + + SlotMapTestSuite.AddTest(max::Testing::Test< max::Testing::CoutResultPolicy >{ "operator[] fetches the element", [](max::Testing::Test< max::Testing::CoutResultPolicy >& CurrentTest, max::Testing::CoutResultPolicy const& ResultPolicy) { + auto test = max::Containers::SlotMap< int >{}; + auto handle = test.push_back(10); + + CurrentTest.MAX_TESTING_ASSERT(test[handle] == 10); + } + }); + + SlotMapTestSuite.AddTest(max::Testing::Test< max::Testing::CoutResultPolicy >{ "const operator[] fetches the element", [](max::Testing::Test< max::Testing::CoutResultPolicy >& CurrentTest, max::Testing::CoutResultPolicy const& ResultPolicy) { + auto test = max::Containers::SlotMap< int >{}; + auto handle = test.push_back(10); + + const auto& const_test = test; + + CurrentTest.MAX_TESTING_ASSERT(const_test[handle] == 10); + } + }); + + SlotMapTestSuite.AddTest(max::Testing::Test< max::Testing::CoutResultPolicy >{ "remove removes the element", [](max::Testing::Test< max::Testing::CoutResultPolicy >& CurrentTest, max::Testing::CoutResultPolicy const& ResultPolicy) { + auto test = max::Containers::SlotMap< int >{}; + auto one_handle = test.push_back(1); + auto two_handle = test.push_back(2); + CurrentTest.MAX_TESTING_ASSERT(test.size() == 2); + + test.remove(one_handle); + + CurrentTest.MAX_TESTING_ASSERT(test.size() == 1); + CurrentTest.MAX_TESTING_ASSERT(test[two_handle] == 2); + } + }); + + SlotMapTestSuite.AddTest(max::Testing::Test< max::Testing::CoutResultPolicy >{ "pop_back removes the last element", [](max::Testing::Test< max::Testing::CoutResultPolicy >& CurrentTest, max::Testing::CoutResultPolicy const& ResultPolicy) { + auto test = max::Containers::SlotMap< int >{}; + auto one_handle = test.push_back(1); + test.push_back(2); + CurrentTest.MAX_TESTING_ASSERT(test.size() == 2); + + test.pop_back(); + + CurrentTest.MAX_TESTING_ASSERT(test.size() == 1); + CurrentTest.MAX_TESTING_ASSERT(test[one_handle] == 1); + } + }); + + SlotMapTestSuite.AddTest(max::Testing::Test< max::Testing::CoutResultPolicy >{ "pop_back removes the last element", [](max::Testing::Test< max::Testing::CoutResultPolicy >& CurrentTest, max::Testing::CoutResultPolicy const& ResultPolicy) { + auto test = max::Containers::SlotMap< int >{}; + auto one_handle = test.push_back(1); + test.push_back(2); + CurrentTest.MAX_TESTING_ASSERT(test.size() == 2); + + test.pop_back(); + + CurrentTest.MAX_TESTING_ASSERT(test.size() == 1); + CurrentTest.MAX_TESTING_ASSERT(test[one_handle] == 1); + } + }); + + SlotMapTestSuite.AddTest(max::Testing::Test< max::Testing::CoutResultPolicy >{ "begin() and end() allow iteration", [](max::Testing::Test< max::Testing::CoutResultPolicy >& CurrentTest, max::Testing::CoutResultPolicy const& ResultPolicy) { + auto test = max::Containers::SlotMap< size_t >{}; + auto one_handle = test.push_back(1); + auto two_handle = test.push_back(2); + + auto element_count = size_t{ 0 }; + + auto end = test.end(); + for (auto it = test.begin(); it != end; ++it) { + element_count++; + *it = 10 * element_count; + } + + CurrentTest.MAX_TESTING_ASSERT(test[one_handle] == 10); + CurrentTest.MAX_TESTING_ASSERT(test[two_handle] == 20); + CurrentTest.MAX_TESTING_ASSERT(element_count == 2); + } + }); + + SlotMapTestSuite.AddTest(max::Testing::Test< max::Testing::CoutResultPolicy >{ "const begin() and const end() allow iteration", [](max::Testing::Test< max::Testing::CoutResultPolicy >& CurrentTest, max::Testing::CoutResultPolicy const& ResultPolicy) { + auto test = max::Containers::SlotMap< int >{}; + test.push_back(1); + test.push_back(2); + const auto& const_test = test; + + auto element_count = size_t{ 0 }; + + auto end = const_test.end(); + for (auto it = const_test.begin(); it != end; ++it) { + element_count++; + } + + CurrentTest.MAX_TESTING_ASSERT(element_count == 2); + } + }); + + SlotMapTestSuite.AddTest(max::Testing::Test< max::Testing::CoutResultPolicy >{ "cbegin() and cend() allow iteration", [](max::Testing::Test< max::Testing::CoutResultPolicy >& CurrentTest, max::Testing::CoutResultPolicy const& ResultPolicy) { + auto test = max::Containers::SlotMap< int >{}; + test.push_back(1); + test.push_back(2); + + auto element_count = size_t{ 0 }; + + auto end = test.cend(); + for (auto it = test.cbegin(); it != end; ++it) { + element_count++; + } + + CurrentTest.MAX_TESTING_ASSERT(element_count == 2); + } + }); + + SlotMapTestSuite.AddTest(max::Testing::Test< max::Testing::CoutResultPolicy >{ "size() returns the element count", [](max::Testing::Test< max::Testing::CoutResultPolicy >& CurrentTest, max::Testing::CoutResultPolicy const& ResultPolicy) { + auto test = max::Containers::SlotMap< int >{}; + + CurrentTest.MAX_TESTING_ASSERT(test.size() == 0); + + test.push_back(1); + + CurrentTest.MAX_TESTING_ASSERT(test.size() == 1); + + test.push_back(2); + + CurrentTest.MAX_TESTING_ASSERT(test.size() == 2); + } + }); + + SlotMapTestSuite.AddTest(max::Testing::Test< max::Testing::CoutResultPolicy >{ "max_size() returns the max element count", [](max::Testing::Test< max::Testing::CoutResultPolicy >& CurrentTest, max::Testing::CoutResultPolicy const& ResultPolicy) { + auto test = max::Containers::SlotMap< int >{}; + + auto max_size = test.max_size(); + + CurrentTest.MAX_TESTING_ASSERT(max_size > test.size()); + + test.push_back(1); + + CurrentTest.MAX_TESTING_ASSERT(max_size == test.max_size()); + } + }); + + SlotMapTestSuite.RunTests(); + } + +} // namespace Containers +} // namespace maxAutomatedTests diff --git a/Code/max/Containers/SlotMapTest.hpp b/Code/max/Containers/SlotMapTest.hpp new file mode 100644 index 0000000..c9aca64 --- /dev/null +++ b/Code/max/Containers/SlotMapTest.hpp @@ -0,0 +1,18 @@ +// Copyright 2026, The max Contributors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MAXAUTOMATEDTESTS_CONTAINERS_SLOTMAPTEST_HPP +#define MAXAUTOMATEDTESTS_CONTAINERS_SLOTMAPTEST_HPP + +namespace maxAutomatedTests +{ +namespace Containers +{ + + void RunSlotMapTestSuite(); + +} // namespace Containers +} // namespace maxAutomatedTests + +#endif // #ifndef MAXAUTOMATEDTESTS_CONTAINERS_SLOTMAPTEST_HPP diff --git a/Code/max/Testing/AutomatedTestsEntryPoint.cpp b/Code/max/Testing/AutomatedTestsEntryPoint.cpp index df4f22e..f8f8091 100644 --- a/Code/max/Testing/AutomatedTestsEntryPoint.cpp +++ b/Code/max/Testing/AutomatedTestsEntryPoint.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include @@ -19,6 +20,7 @@ int main() maxAutomatedTests::Containers::RunPointTestSuite(); maxAutomatedTests::Containers::RunRangeTestSuite(); maxAutomatedTests::Containers::RunRectangleTestSuite(); + maxAutomatedTests::Containers::RunSlotMapTestSuite(); maxAutomatedTests::Containers::RunVectorTestSuite(); return 0; diff --git a/Projects/Make/Makefile b/Projects/Make/Makefile index 661a79a..011d96f 100644 --- a/Projects/Make/Makefile +++ b/Projects/Make/Makefile @@ -74,6 +74,7 @@ AUTOMATED_TEST_CXX_SRCS = \ ../../Code/max/Containers/PointTest.cpp \ ../../Code/max/Containers/RangeTest.cpp \ ../../Code/max/Containers/RectangleTest.cpp \ + ../../Code/max/Containers/SlotMapTest.cpp \ ../../Code/max/Containers/VectorTest.cpp \ ../../Code/max/Containers/StateMachine/AnythingMatcherTest.cpp \ ../../Code/max/Containers/StateMachine/NodeTest.cpp \ diff --git a/Projects/VisualStudio/max/max.vcxproj b/Projects/VisualStudio/max/max.vcxproj index 6658af7..7a9decf 100644 --- a/Projects/VisualStudio/max/max.vcxproj +++ b/Projects/VisualStudio/max/max.vcxproj @@ -51,6 +51,7 @@ + @@ -147,6 +148,13 @@ true true + + + true + true + true + true + true @@ -318,6 +326,12 @@ true true + + true + true + true + true + true true diff --git a/Projects/VisualStudio/max/max.vcxproj.filters b/Projects/VisualStudio/max/max.vcxproj.filters index b3aed8e..4d421cf 100644 --- a/Projects/VisualStudio/max/max.vcxproj.filters +++ b/Projects/VisualStudio/max/max.vcxproj.filters @@ -204,6 +204,9 @@ Code\max\Containers\StateMachine + + Code\max\Containers + @@ -474,6 +477,12 @@ Code\max\Containers\StateMachine + + Code\max\Containers + + + Code\max\Containers + @@ -599,6 +608,9 @@ Code\max\Containers\StateMachine + + Code\max\Containers + diff --git a/Projects/VisualStudio/maxAutomatedTests/maxAutomatedTests.vcxproj b/Projects/VisualStudio/maxAutomatedTests/maxAutomatedTests.vcxproj index 47b3df8..1d2f622 100644 --- a/Projects/VisualStudio/maxAutomatedTests/maxAutomatedTests.vcxproj +++ b/Projects/VisualStudio/maxAutomatedTests/maxAutomatedTests.vcxproj @@ -25,6 +25,7 @@ + @@ -43,6 +44,7 @@ + diff --git a/Projects/VisualStudio/maxAutomatedTests/maxAutomatedTests.vcxproj.filters b/Projects/VisualStudio/maxAutomatedTests/maxAutomatedTests.vcxproj.filters index 9367e49..61ea15d 100644 --- a/Projects/VisualStudio/maxAutomatedTests/maxAutomatedTests.vcxproj.filters +++ b/Projects/VisualStudio/maxAutomatedTests/maxAutomatedTests.vcxproj.filters @@ -63,6 +63,9 @@ Containers\StateMachine + + Containers + @@ -110,5 +113,8 @@ Containers\StateMachine + + Containers + \ No newline at end of file