diff --git a/README.md b/README.md index b14b67c..8669c43 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ ![Version](https://img.shields.io/badge/version-1.1.1-blue.svg) ![Language](https://img.shields.io/badge/C++17-blue.svg) +![Documentation](https://img.shields.io/badge/Doxygen-blue.svg) ![License](https://img.shields.io/badge/license-MIT_License-blue.svg?style=flat) [![CI](https://github.com/karel-burda/constexpr-hash-map/actions/workflows/main.yml/badge.svg)](https://github.com/karel-burda/constexpr-hash-map/actions/workflows/main.yml) @@ -15,10 +16,11 @@ Container supports: * look-up * value retrieval * supports iterators (`cend()`, `std::size()`, ...) -* algorithms (for-each, ...) +* algorithms (for-each, ...) -- note that iteration order is unspecified and depends on hash values Implemented and documented in the [constexpr_hash_map.hpp](include/constexpr_hash_map/constexpr_hash_map.hpp). +TODO: FIXME Behaviour is undefined, if there are multiple same keys. Compatible and tested on: @@ -26,7 +28,7 @@ Compatible and tested on: * x86-64 clang++ 6.0 and higher * x64 MSVC v19.14 and higher -In case of bigger number of elements, compiler's settings regarding constexpr (such as `-fconstexpr-depth` on the GNU) might needed be tuned-up, as container uses compile-time recursion. +In case of higher number of elements, compiler's settings regarding constexpr (such as `-fconstexpr-depth` on the GNU) might needed be tuned-up, as container uses compile-time recursion. # Example ```cpp @@ -65,12 +67,9 @@ for (const auto& [key, value] : map) } ``` -See also [main.cpp](main.cpp). +See also [test/main.cpp](test/main.cpp). -Example might compiled (with no additional flags), for example, by this minimal command: +Tests might compiled (with no additional flags), for example, by this minimal command: ```bash g++ main.cpp -I include -std=c++17 ``` - -# Live Demo -* ```x86-64 g++ 12.1```: **https://godbolt.org/z/rjrxcbWo9** diff --git a/include/constexpr_hash_map/constexpr_hash_map.hpp b/include/constexpr_hash_map/constexpr_hash_map.hpp index c1b9d50..f6c252c 100644 --- a/include/constexpr_hash_map/constexpr_hash_map.hpp +++ b/include/constexpr_hash_map/constexpr_hash_map.hpp @@ -38,8 +38,14 @@ class hash_map explicit constexpr hash_map(E&&... elements) noexcept : data{std::forward(elements)...} { + static_assert(std::is_nothrow_copy_constructible::value, "Key must be noexcept copy constructible"); + static_assert(std::is_nothrow_copy_constructible::value, "Value must be noexcept copy constructible"); + + static_assert(detail::is_constexpr_hashable::value, "Key must be hashable in constexpr context"); + static_assert(N > 0, "N should be positive"); static_assert(N == sizeof...(elements), "Elements size doesn't match expected size of a hash-map"); + static_assert(!has_duplicate_keys<...>(data), "There are duplicate keys"); } /// @brief Searches map for a given key and returns iterator. @@ -168,8 +174,42 @@ class hash_map } private: + template + constexpr bool has_duplicate_keys(const PairArray& data) noexcept + { + for (std::size_t i = 0; i < N; ++i) + { + for (std::size_t j = i + 1; j < N; ++j) + { + if (data[i].first == data[j].first) + { + return true; + } + } + } + return false; + } + data_type data; }; + +namespace detail +{ + template + struct is_constexpr_hashable { + private: + template + static constexpr auto test(int) -> + decltype(constexpr_hash(std::declval()), true); + + template + static constexpr bool test(...) { return false; } + + public: + static constexpr bool value = test(0); + }; +} // namespace detail } // namespace burda::ct #endif // CONSTEXPR_HASH_MAP_CONSTEXPR_HASH_MAP_HPP + diff --git a/tests/constexpr_positive.cpp b/tests/constexpr_positive.cpp new file mode 100644 index 0000000..263f37d --- /dev/null +++ b/tests/constexpr_positive.cpp @@ -0,0 +1,68 @@ +#include + +using burda::ct::hash_map; + +/* + * Basic construction and lookup + */ +static constexpr hash_map<2, const char*, int> basic_map{ + std::make_pair("a", 1), + std::make_pair("b", 2), +}; + +static_assert(basic_map.size() == 2, "size() failed"); +static_assert(basic_map.contains("a"), "contains() failed"); +static_assert(basic_map.contains("b"), "contains() failed"); +static_assert(!basic_map.contains("c"), "contains() false positive"); + +/* + * Safe lookup: get() + */ +static_assert(basic_map.get("a") != nullptr, "get() returned nullptr"); +static_assert(*basic_map.get("a") == 1, "get() returned wrong value"); +static_assert(basic_map.get("c") == nullptr, "get() should return nullptr"); + +/* + * Safe lookup: value_or() + */ +static_assert(basic_map.value_or("a", -1) == 1, "value_or() failed"); +static_assert(basic_map.value_or("c", -1) == -1, "value_or() fallback failed"); + +/* + * operator[] with existing keys + */ +static_assert(basic_map["a"] == 1, "operator[] failed"); +static_assert(basic_map["b"] == 2, "operator[] failed"); + +/* + * Iteration sanity check (order-independent) + */ +constexpr bool iteration_test() { + bool seen_a = false; + bool seen_b = false; + + for (const auto& kv : basic_map) { + if (kv.first == std::string_view("a") && kv.second == 1) + seen_a = true; + if (kv.first == std::string_view("b") && kv.second == 2) + seen_b = true; + } + return seen_a && seen_b; +} + +static_assert(iteration_test(), "iteration failed"); + +/* + * at() API (if present) + */ +constexpr bool at_test() { + const auto r1 = basic_map.at("a"); + const auto r2 = basic_map.at("c"); + + return r1.first && r1.second == 1 + && !r2.first; +} + +static_assert(at_test(), "at() failed"); + +int main() {} diff --git a/main.cpp b/tests/main.cpp similarity index 78% rename from main.cpp rename to tests/main.cpp index 0637797..200c2fa 100644 --- a/main.cpp +++ b/tests/main.cpp @@ -1,55 +1,61 @@ -#include - -#include - -int example_simple() noexcept -{ - static constexpr burda::ct::hash_map<2, const char*, int> map - { - std::make_pair("key1", 1), - std::make_pair("key2", 2) - }; - - static_assert(map.size() == 2); - static_assert(map.contains("key1")); - static_assert(map.at("key1").second == 1); - static_assert(map.contains("key2")); - static_assert(map["key2"] == 2); - static_assert(!map.contains("key3")); - // this would not compile, because because hash map doesn't contain it - //static_assert(map["key3"] == 3); - - // just as an example to observe generated assembly - return map.at("key2").second; -} - -int example_advanced() noexcept -{ - static constexpr burda::ct::hash_map<3, std::string_view, std::string_view> map - { - std::make_pair("key1", "value1"), - std::make_pair("key2", "value2"), - std::make_pair("key3", "value3") - }; - - // container supports iterators in a basic way - static constexpr auto it = map.find("key2"); - static_assert(it != std::cend(map)); - static_assert(it->second == "value2"); - // another example calls - static_assert(!std::empty(map)); - static_assert(std::size(map) == 3); - - for ([[maybe_unused]] const auto& [key, value] : map) - { - // do something with the elements - } - - // dummy example to observe constexpr nature of the generated assembly - return it->second.size(); -} - -int main([[maybe_unused]] const int argc, [[maybe_unused]] const char** argv) -{ - return example_simple() + example_advanced(); -} +#include + +#include + +namespace burda::test +{ +[[nodiscard]] constexpr int simple() noexcept +{ + static constexpr burda::ct::hash_map<2, const char*, int> map + { + std::make_pair("key1", 1), + std::make_pair("key2", 2) + }; + + static_assert(map.size() == 2); + static_assert(map.contains("key1")); + static_assert(map.at("key1").second == 1); + static_assert(map.contains("key2")); + static_assert(map["key2"] == 2); + static_assert(!map.contains("key3")); + // this would not compile, because because hash map doesn't contain it + //static_assert(map["key3"] == 3); + + return map.at("key2").second == 2; +} + +[[nodiscard]] constexpr int advanced() noexcept +{ + static constexpr burda::ct::hash_map<3, std::string_view, std::string_view> map + { + std::make_pair("key1", "value1"), + std::make_pair("key2", "value2"), + std::make_pair("key3", "value3") + }; + + // container supports iterators in a basic way + static constexpr auto it = map.find("key2"); + static_assert(it != std::cend(map)); + static_assert(it->second == "value2"); + // another example calls + static_assert(!std::empty(map)); + static_assert(std::size(map) == 3); + + for ([[maybe_unused]] const auto& [key, value] : map) + { + // do something with the elements + } + + return it->second.size() == 6; +} + +[[nodiscard]] constexpr int run() +{ + return simple() + advanced(); +} +} // namespace burda::test + +int main([[maybe_unused]] const int argc, [[maybe_unused]] const char** argv) +{ + return burda::test::run(); +} diff --git a/tests/tests/constexpr_negative_duplicate_keys.cpp b/tests/tests/constexpr_negative_duplicate_keys.cpp new file mode 100644 index 0000000..9cc059b --- /dev/null +++ b/tests/tests/constexpr_negative_duplicate_keys.cpp @@ -0,0 +1,10 @@ +#include + +using burda::ct::hash_map; + +static constexpr hash_map<2, const char*, int> duplicate_keys{ + std::make_pair("x", 1), + std::make_pair("x", 2), // duplicate key → compile-time error +}; + +int main() {} diff --git a/tests/tests/constexpr_negative_missing_key.cpp b/tests/tests/constexpr_negative_missing_key.cpp new file mode 100644 index 0000000..d2781d0 --- /dev/null +++ b/tests/tests/constexpr_negative_missing_key.cpp @@ -0,0 +1,12 @@ +#include + +using burda::ct::hash_map; + +static constexpr hash_map<1, const char*, int> map{ + std::make_pair("a", 1), +}; + +// Missing key → compile-time error +static_assert(map["missing"] == 0); + +int main() {}