diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 609c666505..bd3a426181 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -500,6 +500,7 @@ set(SLIC3R_SOURCES Arachne/SkeletalTrapezoidationJoint.hpp Arachne/WallToolPaths.hpp Arachne/WallToolPaths.cpp + StaticMap.hpp ) add_library(libslic3r STATIC ${SLIC3R_SOURCES}) diff --git a/src/libslic3r/StaticMap.hpp b/src/libslic3r/StaticMap.hpp new file mode 100644 index 0000000000..0ef6c840d3 --- /dev/null +++ b/src/libslic3r/StaticMap.hpp @@ -0,0 +1,322 @@ +#ifndef PRUSASLICER_STATICMAP_HPP +#define PRUSASLICER_STATICMAP_HPP + +#include +#include +#include + +namespace Slic3r { + +// This module provides std::map and std::set like structures with fixed number +// of elements and usable at compile or in time constexpr contexts without +// any memory allocations. + +// C++20 emulation utilities to get the missing constexpr functionality in C++17 +namespace static_set_detail { + +// Simple bubble sort but constexpr +template> +constexpr void sort_array(std::array &arr, Cmp cmp = {}) +{ + // A bubble sort will do the job, C++20 will have constexpr std::sort + for (size_t i = 0; i < N - 1; ++i) + { + for (size_t j = 0; j < N - i - 1; ++j) + { + if (!cmp(arr[j], arr[j + 1])) { + T temp = arr[j]; + arr[j] = arr[j + 1]; + arr[j + 1] = temp; + } + } + } +} + +// Simple emulation of lower_bound with constexpr +template> +constexpr auto array_lower_bound(It from, It to, const V &val, Cmp cmp) +{ + auto N = std::distance(from, to); + std::size_t middle = N / 2; + + if (N == 0) { + return from; // Key not found, return the beginning of the array + } else if (cmp(val, *(from + middle))) { + return array_lower_bound(from, from + middle, val, cmp); + } else if (cmp(*(from + middle), val)) { + return array_lower_bound(from + middle + 1, to, val, cmp); + } else { + return from + middle; // Key found, return an iterator to it + } + + return to; +} + +template> +constexpr auto array_lower_bound(const std::array &arr, + const T &val, + Cmp cmp = {}) +{ + return array_lower_bound(arr.begin(), arr.end(), val, cmp); +} + +template +constexpr std::array, N> +to_array_impl(T (&a)[N], std::index_sequence) +{ + return {{a[I]...}}; +} + +template +constexpr std::array, N> to_array(T (&a)[N]) +{ + return to_array_impl(a, std::make_index_sequence{}); +} + +// Emulating constexpr std::pair +template +struct StaticMapElement { + using KeyType = K; + + constexpr StaticMapElement(const K &k, const V &v): first{k}, second{v} {} + + K first; + V second; +}; + +} // namespace static_set_detail +} // namespace Slic3r + +namespace Slic3r { + +// std::set like set structure +template> +class StaticSet { + std::array m_vals; // building on top of std::array + Cmp m_cmp; + +public: + using value_type = T; + + constexpr StaticSet(const std::array &arr, Cmp cmp = {}) + : m_vals{arr}, m_cmp{cmp} + { + // TODO: C++20 can use std::sort(vals.begin(), vals.end()) + static_set_detail::sort_array(m_vals, m_cmp); + } + + template + constexpr StaticSet(Ts &&...args): m_vals{std::forward(args)...} + { + static_set_detail::sort_array(m_vals, m_cmp); + } + + template + constexpr StaticSet(Cmp cmp, Ts &&...args) + : m_vals{std::forward(args)...}, m_cmp{cmp} + { + static_set_detail::sort_array(m_vals, m_cmp); + } + + constexpr auto find(const T &val) const + { + // TODO: C++20 can use std::lower_bound + auto it = static_set_detail::array_lower_bound(m_vals, val, m_cmp); + if (it != m_vals.end() && ! m_cmp(*it, val) && !m_cmp(val, *it) ) + return it; + + return m_vals.cend(); + } + + constexpr bool empty() const { return m_vals.empty(); } + constexpr size_t size() const { return m_vals.size(); } + + // Can be iterated over + constexpr auto begin() const { return m_vals.begin(); } + constexpr auto end() const { return m_vals.end(); } +}; + +// These are "deduction guides", a C++17 feature. +// Reason is to be able to deduce template arguments from constructor arguments +// e.g.: StaticSet{1, 2, 3} is deduced as StaticSet>, no +// need to state the template types explicitly. +template +StaticSet(T, Vals...) -> + StaticSet && ...), T>, + 1 + sizeof...(Vals)>; + +// Same as above, only with the first argument being a comparison functor +template +StaticSet(Cmp, T, Vals...) -> + StaticSet && ...), T>, + 1 + sizeof...(Vals), + std::enable_if_t, Cmp>>; + +// Specialization for the empty set case. +template +class StaticSet { +public: + constexpr StaticSet() = default; + constexpr auto find(const T &val) const { return nullptr; } + constexpr bool empty() const { return true; } + constexpr size_t size() const { return 0; } + constexpr auto begin() const { return nullptr; } + constexpr auto end() const { return nullptr; } +}; + +// Constructor with no arguments need to be deduced as the specialization for +// empty sets (see above) +StaticSet() -> StaticSet; + + + +// StaticMap definition: + +template +using SMapEl = static_set_detail::StaticMapElement; + +template +struct DefaultCmp { + constexpr bool operator() (const SMapEl &el1, const SMapEl &el2) const + { + return std::less{}(el1.first, el2.first); + } +}; + +// Overriding the default comparison for C style strings, as std::less +// doesn't do the lexicographic comparisons, only the pointer values would be +// compared. Fortunately we can wrap the C style strings with string_views and +// do the comparison with those. +template +struct DefaultCmp { + constexpr bool operator() (const SMapEl &el1, + const SMapEl &el2) const + { + return std::string_view{el1.first} < std::string_view{el2.first}; + } +}; + +template> +class StaticMap { + std::array, N> m_vals; + Cmp m_cmp; + +public: + using value_type = SMapEl; + + constexpr StaticMap(const std::array, N> &arr, Cmp cmp = {}) + : m_vals{arr}, m_cmp{cmp} + { + static_set_detail::sort_array(m_vals, cmp); + } + + constexpr auto find(const K &key) const + { + auto ret = m_vals.end(); + + SMapEl vkey{key, V{}}; + + auto it = static_set_detail::array_lower_bound( + std::begin(m_vals), std::end(m_vals), vkey, m_cmp + ); + + if (it != std::end(m_vals) && ! m_cmp(*it, vkey) && !m_cmp(vkey, *it)) + ret = it; + + return ret; + } + + constexpr const V& at(const K& key) const + { + if (auto it = find(key); it != end()) + return it->second; + + throw std::out_of_range{"No such element"}; + } + + constexpr bool empty() const { return m_vals.empty(); } + constexpr size_t size() const { return m_vals.size(); } + + constexpr auto begin() const { return m_vals.begin(); } + constexpr auto end() const { return m_vals.end(); } +}; + +template +class StaticMap { +public: + constexpr StaticMap() = default; + constexpr auto find(const K &key) const { return nullptr; } + constexpr bool empty() const { return true; } + constexpr size_t size() const { return 0; } + [[noreturn]] constexpr const V& at(const K &) const { throw std::out_of_range{"Map is empty"}; } + constexpr auto begin() const { return nullptr; } + constexpr auto end() const { return nullptr; } +}; + +// Deducing template arguments from the StaticMap constructors is not easy, +// so there is a helper "make" function to be used instead: +// e.g.: auto map = make_staticmap({ {"one", 1}, {"two", 2}}) +// will work, and only the key and value type needs to be specified. No need +// to state the number of elements, that is deduced automatically. +template> +constexpr auto make_staticmap(const SMapEl (&arr) [N], Cmp cmp = {}) +{ + return StaticMap{static_set_detail ::to_array(arr), cmp}; +} + +// Override for empty maps +template> +constexpr auto make_staticmap() +{ + return StaticMap{}; +} + +// Override which uses a c++ array as the initializer +template> +constexpr auto make_staticmap(const std::array, N> &arr, Cmp cmp = {}) +{ + return StaticMap{arr, cmp}; +} + +// Helper function to get a specific element from a set, returning a std::optional +// which is more convinient than working with iterators +template +constexpr std::enable_if_t, std::optional> +query(const StaticSet &sset, const T &val) +{ + std::optional ret; + if (auto it = sset.find(val); it != sset.end()) + ret = *it; + + return ret; +} + +template +constexpr std::enable_if_t, std::optional> +query(const StaticMap &sset, const KeyT &val) +{ + std::optional ret; + + if (auto it = sset.find(val); it != sset.end()) + ret = it->second; + + return ret; +} + +template +constexpr std::enable_if_t, bool> +contains(const StaticSet &sset, const T &val) +{ + return sset.find(val) != sset.end(); +} + +template +constexpr std::enable_if_t, bool> +contains(const StaticMap &smap, const KeyT &key) +{ + return smap.find(key) != smap.end(); +} + +} // namespace Slic3r + +#endif // STATICMAP_HPP diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 7b0d200c7d..d065ddb50f 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -42,6 +42,7 @@ add_executable(${_TEST_NAME}_tests test_support_spots_generator.cpp ../data/prusaparts.cpp ../data/prusaparts.hpp + test_static_map.cpp ) if (TARGET OpenVDB::openvdb) diff --git a/tests/libslic3r/test_static_map.cpp b/tests/libslic3r/test_static_map.cpp new file mode 100644 index 0000000000..2ff5178f50 --- /dev/null +++ b/tests/libslic3r/test_static_map.cpp @@ -0,0 +1,98 @@ +#include +#include + +#include "libslic3r/StaticMap.hpp" + +TEST_CASE("Empty static map should be possible to create and should be empty", "[StaticMap]") +{ + using namespace Slic3r; + + static const constexpr StaticSet EmptySet; + + static const constexpr auto EmptyMap = make_staticmap(); + + constexpr bool is_map_empty = EmptyMap.empty(); + constexpr bool is_set_empty = EmptySet.empty(); + + REQUIRE(is_map_empty); + REQUIRE(is_set_empty); +} + +TEST_CASE("StaticSet should derive it's type from the initializer", "[StaticMap]") { + using namespace Slic3r; + static const constexpr StaticSet iOneSet = { 1 }; + static constexpr size_t iOneSetSize = iOneSet.size(); + + REQUIRE(iOneSetSize == 1); + + static const constexpr StaticSet iManySet = { 1, 3, 5, 80, 40 }; + static constexpr size_t iManySetSize = iManySet.size(); + + REQUIRE(iManySetSize == 5); +} + +TEST_CASE("StaticMap should derive it's type using make_staticmap", "[StaticMap]") { + using namespace Slic3r; + static const constexpr auto ciOneMap = make_staticmap({ + {'a', 1}, + }); + + static constexpr size_t ciOneMapSize = ciOneMap.size(); + static constexpr bool ciOneMapValid = query(ciOneMap, 'a').value_or(0) == 1; + + REQUIRE(ciOneMapSize == 1); + REQUIRE(ciOneMapValid); + + static const constexpr auto ciManyMap = make_staticmap({ + {'a', 1}, {'b', 2}, {'A', 10} + }); + + static constexpr size_t ciManyMapSize = ciManyMap.size(); + static constexpr bool ciManyMapValid = + query(ciManyMap, 'a').value_or(0) == 1 && + query(ciManyMap, 'b').value_or(0) == 2 && + query(ciManyMap, 'A').value_or(0) == 10 && + !contains(ciManyMap, 'B') && + !query(ciManyMap, 'c').has_value(); + + REQUIRE(ciManyMapSize == 3); + REQUIRE(ciManyMapValid); + + for (auto &[k, v] : ciManyMap) { + auto val = query(ciManyMap, k); + REQUIRE(val.has_value()); + REQUIRE(val.value() == v); + } +} + +TEST_CASE("StaticSet should be able to find contained values", "[StaticMap]") +{ + using namespace Slic3r; + using namespace std::string_view_literals; + + auto cmp = [](const char *a, const char *b) constexpr { + return std::string_view{a} < std::string_view{b}; + }; + + static constexpr StaticSet CStrSet = {cmp, "One", "Two", "Three"}; + static constexpr StaticSet StringSet = {"One"sv, "Two"sv, "Three"sv}; + + static constexpr bool CStrSetValid = query(CStrSet, "One").has_value() && + contains(CStrSet, "Two") && + contains(CStrSet, "Three") && + !contains(CStrSet, "one") && + !contains(CStrSet, "two") && + !contains(CStrSet, "three"); + + static constexpr bool StringSetValid = contains(StringSet, "One"sv) && + contains(StringSet, "Two"sv) && + contains(StringSet, "Three"sv) && + !contains(StringSet, "one"sv) && + !contains(StringSet, "two"sv) && + !contains(StringSet, "three"sv); + + REQUIRE(CStrSetValid); + REQUIRE(StringSetValid); + REQUIRE(CStrSet.size() == 3); + REQUIRE(StringSet.size() == 3); +}