diff --git a/src/include/robin_hood.h b/src/include/robin_hood.h index 2c598fe9..28fb514b 100644 --- a/src/include/robin_hood.h +++ b/src/include/robin_hood.h @@ -6,7 +6,7 @@ // _/_____/ // // Fast & memory efficient hashtable based on robin hood hashing for C++11/14/17/20 -// version 3.4.4 +// version 3.5.0 // https://github.com/martinus/robin-hood-hashing // // Licensed under the MIT License . @@ -36,8 +36,8 @@ // see https://semver.org/ #define ROBIN_HOOD_VERSION_MAJOR 3 // for incompatible API changes -#define ROBIN_HOOD_VERSION_MINOR 4 // for adding functionality in a backwards-compatible manner -#define ROBIN_HOOD_VERSION_PATCH 4 // for backwards-compatible bug fixes +#define ROBIN_HOOD_VERSION_MINOR 5 // for adding functionality in a backwards-compatible manner +#define ROBIN_HOOD_VERSION_PATCH 0 // for backwards-compatible bug fixes #include #include @@ -846,22 +846,24 @@ struct WrapKeyEqual : public T { // https://www.reddit.com/r/cpp/comments/ahp6iu/compile_time_binary_size_reductions_and_cs_future/eeguck4/ template -class unordered_map - : public WrapHash, - public WrapKeyEqual, - detail::NodeAllocator< - robin_hood::pair::type, T>, 4, 16384, - IsFlatMap> { +class Table : public WrapHash, + public WrapKeyEqual, + detail::NodeAllocator< + typename std::conditional< + std::is_void::value, Key, + robin_hood::pair::type, + T>>::type, + 4, 16384, IsFlatMap> { public: using key_type = Key; using mapped_type = T; - using value_type = - robin_hood::pair::type, T>; + using value_type = typename std::conditional< + std::is_void::value, Key, + robin_hood::pair::type, T>>::type; using size_type = size_t; using hasher = Hash; using key_equal = KeyEqual; - using Self = - unordered_map; + using Self = Table; static constexpr bool is_flat_map = IsFlatMap; private: @@ -923,19 +925,40 @@ class unordered_map return mData; } - ROBIN_HOOD(NODISCARD) typename value_type::first_type& getFirst() noexcept { + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::value, typename V::first_type&>::type + getFirst() noexcept { return mData.first; } + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::value, V&>::type getFirst() noexcept { + return mData; + } - ROBIN_HOOD(NODISCARD) typename value_type::first_type const& getFirst() const noexcept { + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::value, typename V::first_type const&>::type + getFirst() const noexcept { return mData.first; } + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::value, V const&>::type getFirst() const noexcept { + return mData; + } - ROBIN_HOOD(NODISCARD) typename value_type::second_type& getSecond() noexcept { + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::value, Q&>::type getSecond() noexcept { return mData.second; } - ROBIN_HOOD(NODISCARD) typename value_type::second_type const& getSecond() const noexcept { + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::value, Q const&>::type getSecond() const + noexcept { return mData.second; } @@ -987,19 +1010,40 @@ class unordered_map return *mData; } - ROBIN_HOOD(NODISCARD) typename value_type::first_type& getFirst() { + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::value, typename V::first_type&>::type + getFirst() noexcept { return mData->first; } + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::value, V&>::type getFirst() noexcept { + return *mData; + } - ROBIN_HOOD(NODISCARD) typename value_type::first_type const& getFirst() const { + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::value, typename V::first_type const&>::type + getFirst() const noexcept { return mData->first; } + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::value, V const&>::type getFirst() const noexcept { + return *mData; + } - ROBIN_HOOD(NODISCARD) typename value_type::second_type& getSecond() { + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::value, Q&>::type getSecond() noexcept { return mData->second; } - ROBIN_HOOD(NODISCARD) typename value_type::second_type const& getSecond() const { + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::value, Q const&>::type getSecond() const + noexcept { return mData->second; } @@ -1014,6 +1058,25 @@ class unordered_map using Node = DataNode; + // helpers for doInsert: extract first entry (only const required) + ROBIN_HOOD(NODISCARD) key_type const& getFirstConst(Node const& n) const noexcept { + return n.getFirst(); + } + + // in case we have void mapped_type, we are not using a pair, thus we just route k through. + // No need to disable this because it's just not used if not applicable. + ROBIN_HOOD(NODISCARD) key_type const& getFirstConst(key_type const& k) const noexcept { + return k; + } + + // in case we have non-void mapped_type, we have a standard robin_hood::pair + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::value, key_type const&>::type + getFirstConst(value_type const& vt) const noexcept { + return vt.first; + } + // Cloner ////////////////////////////////////////////////////////// template @@ -1184,8 +1247,7 @@ class unordered_map } while (inc == static_cast(sizeof(size_t))); } - friend class unordered_map; + friend class Table; NodePtr mKeyVals{nullptr}; uint8_t const* mInfo{nullptr}; }; @@ -1291,8 +1353,8 @@ class unordered_map mKeyVals, reinterpret_cast_no_cast_align_warning(mInfo))); } - void cloneData(const unordered_map& o) { - Cloner()(o, *this); + void cloneData(const Table& o) { + Cloner()(o, *this); } // inserts a keyval that is guaranteed to be new, e.g. when the hashmap is resized. @@ -1350,34 +1412,33 @@ class unordered_map // payed at the first insert, and not before. Lookup of this empty map works because everybody // points to DummyInfoByte::b. parameter bucket_count is dictated by the standard, but we can // ignore it. - explicit unordered_map(size_t ROBIN_HOOD_UNUSED(bucket_count) /*unused*/ = 0, - const Hash& h = Hash{}, - const KeyEqual& equal = KeyEqual{}) noexcept(noexcept(Hash(h)) && - noexcept(KeyEqual(equal))) + explicit Table(size_t ROBIN_HOOD_UNUSED(bucket_count) /*unused*/ = 0, const Hash& h = Hash{}, + const KeyEqual& equal = KeyEqual{}) noexcept(noexcept(Hash(h)) && + noexcept(KeyEqual(equal))) : WHash(h) , WKeyEqual(equal) { ROBIN_HOOD_TRACE(this); } template - unordered_map(Iter first, Iter last, size_t ROBIN_HOOD_UNUSED(bucket_count) /*unused*/ = 0, - const Hash& h = Hash{}, const KeyEqual& equal = KeyEqual{}) + Table(Iter first, Iter last, size_t ROBIN_HOOD_UNUSED(bucket_count) /*unused*/ = 0, + const Hash& h = Hash{}, const KeyEqual& equal = KeyEqual{}) : WHash(h) , WKeyEqual(equal) { ROBIN_HOOD_TRACE(this); insert(first, last); } - unordered_map(std::initializer_list initlist, - size_t ROBIN_HOOD_UNUSED(bucket_count) /*unused*/ = 0, const Hash& h = Hash{}, - const KeyEqual& equal = KeyEqual{}) + Table(std::initializer_list initlist, + size_t ROBIN_HOOD_UNUSED(bucket_count) /*unused*/ = 0, const Hash& h = Hash{}, + const KeyEqual& equal = KeyEqual{}) : WHash(h) , WKeyEqual(equal) { ROBIN_HOOD_TRACE(this); insert(initlist.begin(), initlist.end()); } - unordered_map(unordered_map&& o) noexcept + Table(Table&& o) noexcept : WHash(std::move(static_cast(o))) , WKeyEqual(std::move(static_cast(o))) , DataPool(std::move(static_cast(o))) { @@ -1395,7 +1456,7 @@ class unordered_map } } - unordered_map& operator=(unordered_map&& o) noexcept { + Table& operator=(Table&& o) noexcept { ROBIN_HOOD_TRACE(this); if (&o != this) { if (o.mMask) { @@ -1422,7 +1483,7 @@ class unordered_map return *this; } - unordered_map(const unordered_map& o) + Table(const Table& o) : WHash(static_cast(o)) , WKeyEqual(static_cast(o)) , DataPool(static_cast(o)) { @@ -1446,7 +1507,7 @@ class unordered_map } // Creates a copy of the given map. Copy constructor of each entry is used. - unordered_map& operator=(unordered_map const& o) { + Table& operator=(Table const& o) { ROBIN_HOOD_TRACE(this); if (&o == this) { // prevent assigning of itself @@ -1504,7 +1565,7 @@ class unordered_map } // Swaps everything between the two maps. - void swap(unordered_map& o) { + void swap(Table& o) { ROBIN_HOOD_TRACE(this); using std::swap; swap(o, *this); @@ -1532,13 +1593,13 @@ class unordered_map } // Destroys the map and all it's contents. - ~unordered_map() { + ~Table() { ROBIN_HOOD_TRACE(this); destroy(); } // Checks if both maps contain the same entries. Order is irrelevant. - bool operator==(const unordered_map& other) const { + bool operator==(const Table& other) const { ROBIN_HOOD_TRACE(this); if (other.size() != size()) { return false; @@ -1553,17 +1614,19 @@ class unordered_map return true; } - bool operator!=(const unordered_map& other) const { + bool operator!=(const Table& other) const { ROBIN_HOOD_TRACE(this); return !operator==(other); } - mapped_type& operator[](const key_type& key) { + template + typename std::enable_if::value, Q&>::type operator[](const key_type& key) { ROBIN_HOOD_TRACE(this); return doCreateByKey(key); } - mapped_type& operator[](key_type&& key) { + template + typename std::enable_if::value, Q&>::type operator[](key_type&& key) { ROBIN_HOOD_TRACE(this); return doCreateByKey(std::move(key)); } @@ -1610,7 +1673,9 @@ class unordered_map // Returns a reference to the value found for key. // Throws std::out_of_range if element cannot be found - mapped_type& at(key_type const& key) { + template + // NOLINTNEXTLINE(modernize-use-nodiscard) + typename std::enable_if::value, Q&>::type at(key_type const& key) { ROBIN_HOOD_TRACE(this); auto kv = mKeyVals + findIdx(key); if (kv == reinterpret_cast_no_cast_align_warning(mInfo)) { @@ -1621,7 +1686,9 @@ class unordered_map // Returns a reference to the value found for key. // Throws std::out_of_range if element cannot be found - mapped_type const& at(key_type const& key) const { // NOLINT(modernize-use-nodiscard) + template + // NOLINTNEXTLINE(modernize-use-nodiscard) + typename std::enable_if::value, Q const&>::type at(key_type const& key) const { ROBIN_HOOD_TRACE(this); auto kv = mKeyVals + findIdx(key); if (kv == reinterpret_cast_no_cast_align_warning(mInfo)) { @@ -1883,8 +1950,8 @@ class unordered_map mInfoHashShift = InitialInfoHashShift; } - template - mapped_type& doCreateByKey(Arg&& key) { + template + typename std::enable_if::value, Q&>::type doCreateByKey(Arg&& key) { while (true) { size_t idx; InfoType info; @@ -1946,12 +2013,12 @@ class unordered_map while (true) { size_t idx; InfoType info; - keyToIdx(keyval.getFirst(), &idx, &info); + keyToIdx(getFirstConst(keyval), &idx, &info); nextWhileLess(&info, &idx); // while we potentially have a match while (info == mInfo[idx]) { - if (WKeyEqual::operator()(keyval.getFirst(), mKeyVals[idx].getFirst())) { + if (WKeyEqual::operator()(getFirstConst(keyval), mKeyVals[idx].getFirst())) { // key already exists, do NOT insert. // see http://en.cppreference.com/w/cpp/container/unordered_map/insert return std::make_pair(iterator(mKeyVals + idx, mInfo + idx), @@ -2087,21 +2154,40 @@ class unordered_map } // namespace detail +// map + template , typename KeyEqual = std::equal_to, size_t MaxLoadFactor100 = 80> -using unordered_flat_map = detail::unordered_map; +using unordered_flat_map = detail::Table; template , typename KeyEqual = std::equal_to, size_t MaxLoadFactor100 = 80> -using unordered_node_map = detail::unordered_map; +using unordered_node_map = detail::Table; template , typename KeyEqual = std::equal_to, size_t MaxLoadFactor100 = 80> using unordered_map = - detail::unordered_map) <= sizeof(size_t) * 6 && - std::is_nothrow_move_constructible>::value && - std::is_nothrow_move_assignable>::value, - MaxLoadFactor100, Key, T, Hash, KeyEqual>; + detail::Table) <= sizeof(size_t) * 6 && + std::is_nothrow_move_constructible>::value && + std::is_nothrow_move_assignable>::value, + MaxLoadFactor100, Key, T, Hash, KeyEqual>; + +// set + +template , typename KeyEqual = std::equal_to, + size_t MaxLoadFactor100 = 80> +using unordered_flat_set = detail::Table; + +template , typename KeyEqual = std::equal_to, + size_t MaxLoadFactor100 = 80> +using unordered_node_set = detail::Table; + +template , + typename KeyEqual = std::equal_to, size_t MaxLoadFactor100 = 80> +using unordered_set = detail::Table::value && + std::is_nothrow_move_assignable::value, + MaxLoadFactor100, Key, void, Hash, KeyEqual>; } // namespace robin_hood diff --git a/src/test/unit/CMakeLists.txt b/src/test/unit/CMakeLists.txt index b701480c..236ea4c9 100644 --- a/src/test/unit/CMakeLists.txt +++ b/src/test/unit/CMakeLists.txt @@ -8,7 +8,8 @@ target_sources_local(rh PRIVATE bench_hash_int.cpp bench_hash_string.cpp bench_iterate.cpp - bench_quick_overall.cpp + bench_quick_overall_map.cpp + bench_quick_overall_set.cpp bench_random_insert_erase.cpp # count @@ -68,5 +69,6 @@ target_sources_local(rh PRIVATE unit_sizeof.cpp unit_undefined_behavior_nekrolm.cpp unit_unique_ptr.cpp + unit_unordered_set.cpp unit_vectorofmaps.cpp ) diff --git a/src/test/unit/bench_quick_overall.cpp b/src/test/unit/bench_quick_overall_map.cpp similarity index 83% rename from src/test/unit/bench_quick_overall.cpp rename to src/test/unit/bench_quick_overall_map.cpp index f58a97aa..59f0685a 100644 --- a/src/test/unit/bench_quick_overall.cpp +++ b/src/test/unit/bench_quick_overall_map.cpp @@ -61,8 +61,8 @@ void benchRandomInsertErase(ankerl::nanobench::Config* cfg) { verifier += map.erase(key); } } - REQUIRE(verifier == 1996311U); - REQUIRE(map.size() == 9966U); + CHECK(verifier == 1996311U); + CHECK(map.size() == 9966U); }); } @@ -95,7 +95,7 @@ void benchIterate(ankerl::nanobench::Config* cfg) { } } while (!map.empty()); - REQUIRE(result == 62343599601U); + CHECK(result == 62343599601U); }); } @@ -135,9 +135,9 @@ void benchRandomFind(ankerl::nanobench::Config* cfg) { } } - REQUIRE(checksum == 12570603553U); - REQUIRE(found == 4972187U); - REQUIRE(notFound == 5027813U); + CHECK(checksum == 12570603553U); + CHECK(found == 4972187U); + CHECK(notFound == 5027813U); }); } @@ -158,7 +158,7 @@ double geomean1(ankerl::nanobench::Config const& cfg) { // A relatively quick benchmark that should get a relatively good single number of how good the map // is. It calculates geometric mean of several benchmarks. -TEST_CASE("bench_quick_overall_flat" * doctest::test_suite("bench") * doctest::skip()) { +TEST_CASE("bench_quick_overall_map_flat" * doctest::test_suite("bench") * doctest::skip()) { ankerl::nanobench::Config cfg; benchAll>(&cfg); benchAll>(&cfg); @@ -169,7 +169,7 @@ TEST_CASE("bench_quick_overall_flat" * doctest::test_suite("bench") * doctest::s #endif } -TEST_CASE("bench_quick_overall_node" * doctest::test_suite("bench") * doctest::skip()) { +TEST_CASE("bench_quick_overall_map_node" * doctest::test_suite("bench") * doctest::skip()) { ankerl::nanobench::Config cfg; benchAll>(&cfg); benchAll>(&cfg); @@ -180,9 +180,23 @@ TEST_CASE("bench_quick_overall_node" * doctest::test_suite("bench") * doctest::s #endif } -TEST_CASE("bench_quick_overall_std" * doctest::test_suite("bench") * doctest::skip()) { +TEST_CASE("bench_quick_overall_map_std" * doctest::test_suite("bench") * doctest::skip()) { ankerl::nanobench::Config cfg; benchAll>(&cfg); benchAll>(&cfg); std::cout << geomean1(cfg) << std::endl; } + +// 3345972 kb unordered_flat_map +// 2616020 kb unordered_node_map +// 4071892 kb std::unordered_map +TEST_CASE_TEMPLATE("memory_map_huge" * doctest::test_suite("bench") * doctest::skip(), Map, + robin_hood::unordered_flat_map, + robin_hood::unordered_node_map, + std::unordered_map) { + Map map; + for (uint64_t n = 0; n < 80000000; ++n) { + map[n]; + } + std::cout << map.size() << std::endl; +} diff --git a/src/test/unit/bench_quick_overall_set.cpp b/src/test/unit/bench_quick_overall_set.cpp new file mode 100644 index 00000000..2083b6b0 --- /dev/null +++ b/src/test/unit/bench_quick_overall_set.cpp @@ -0,0 +1,208 @@ +//#define ROBIN_HOOD_COUNT_ENABLED + +#include + +#include +#include +#include +#include +#include + +#include + +TYPE_TO_STRING(robin_hood::unordered_flat_set); +TYPE_TO_STRING(robin_hood::unordered_flat_set); +TYPE_TO_STRING(robin_hood::unordered_node_set); +TYPE_TO_STRING(robin_hood::unordered_node_set); +TYPE_TO_STRING(std::unordered_set); +TYPE_TO_STRING(std::unordered_set); + +namespace { + +template +inline K initKey(); + +template <> +inline uint64_t initKey() { + return {}; +} +inline void randomizeKey(sfc64* rng, int n, uint64_t* key) { + // we limit ourselfes to 32bit n + auto limited = (((*rng)() >> 32U) * static_cast(n)) >> 32U; + *key = limited; +} +inline size_t getSizeT(uint64_t val) { + return static_cast(val); +} +inline size_t getSizeT(std::string const& str) { + uint64_t x; + std::memcpy(&x, str.data(), sizeof(uint64_t)); + return static_cast(x); +} + +template <> +inline std::string initKey() { + std::string str; + str.resize(200); + return str; +} +inline void randomizeKey(sfc64* rng, int n, std::string* key) { + uint64_t k; + randomizeKey(rng, n, &k); + std::memcpy(&(*key)[0], &k, sizeof(k)); +} + +// Random insert & erase +template +void benchRandomInsertErase(ankerl::nanobench::Config* cfg) { + cfg->run(type_string(Set{}) + " random insert erase", [&] { + sfc64 rng(123); + size_t verifier{}; + Set set; + auto key = initKey(); + for (int n = 1; n < 20000; ++n) { + for (int i = 0; i < 200; ++i) { + randomizeKey(&rng, n, &key); + set.insert(key); + randomizeKey(&rng, n, &key); + verifier += set.erase(key); + } + } + CHECK(verifier == 1996311U); + CHECK(set.size() == 9966U); + }); +} + +// iterate +template +void benchIterate(ankerl::nanobench::Config* cfg) { + size_t numElements = 5000; + + auto key = initKey(); + + // insert + cfg->run(type_string(Set{}) + " iterate while adding then removing", [&] { + sfc64 rng(555); + Set set; + size_t result = 0; + for (size_t n = 0; n < numElements; ++n) { + randomizeKey(&rng, 1000000, &key); + set.insert(key); + for (auto const& keyVal : set) { + result += getSizeT(keyVal); + } + } + + rng.seed(555); + do { + randomizeKey(&rng, 1000000, &key); + set.erase(key); + for (auto const& keyVal : set) { + result += getSizeT(keyVal); + } + } while (!set.empty()); + + CHECK(result == 12620652940000U); + }); +} + +template +void benchRandomFind(ankerl::nanobench::Config* cfg) { + cfg->run(type_string(Set{}) + " 50% probability to find", [&] { + sfc64 numberesInsertRng(222); + sfc64 numbersSearchRng(222); + + sfc64 insertionRng(123); + + size_t checksum = 0; + size_t found = 0; + size_t notFound = 0; + + Set set; + auto key = initKey(); + for (size_t i = 0; i < 10000; ++i) { + randomizeKey(&numberesInsertRng, 1000000, &key); + if (insertionRng.uniform(100) < 50) { + set.insert(key); + } + + // search 1000 entries in the set + for (size_t search = 0; search < 1000; ++search) { + randomizeKey(&numbersSearchRng, 1000000, &key); + auto it = set.find(key); + if (it != set.end()) { + checksum += getSizeT(*it); + ++found; + } else { + ++notFound; + } + if (numbersSearchRng.counter() == numberesInsertRng.counter()) { + numbersSearchRng.seed(222); + } + } + } + + CHECK(checksum == 2551566706382U); + CHECK(found == 4972187U); + CHECK(notFound == 5027813U); + }); +} + +template +void benchAll(ankerl::nanobench::Config* cfg) { + cfg->title("benchmarking"); + benchIterate(cfg); + benchRandomInsertErase(cfg); + benchRandomFind(cfg); +} + +double geomean1(ankerl::nanobench::Config const& cfg) { + return geomean(cfg.results(), + [](ankerl::nanobench::Result const& result) { return result.median().count(); }); +} + +} // namespace + +// A relatively quick benchmark that should get a relatively good single number of how good the set +// is. It calculates geometric mean of several benchmarks. +TEST_CASE("bench_quick_overall_set_flat" * doctest::test_suite("bench") * doctest::skip()) { + ankerl::nanobench::Config cfg; + benchAll>(&cfg); + benchAll>(&cfg); + std::cout << geomean1(cfg) << std::endl; + +#ifdef ROBIN_HOOD_COUNT_ENABLED + std::cout << robin_hood::counts() << std::endl; +#endif +} + +TEST_CASE("bench_quick_overall_set_node" * doctest::test_suite("bench") * doctest::skip()) { + ankerl::nanobench::Config cfg; + benchAll>(&cfg); + benchAll>(&cfg); + std::cout << geomean1(cfg) << std::endl; + +#ifdef ROBIN_HOOD_COUNT_ENABLED + std::cout << robin_hood::counts() << std::endl; +#endif +} + +TEST_CASE("bench_quick_overall_set_std" * doctest::test_suite("bench") * doctest::skip()) { + ankerl::nanobench::Config cfg; + benchAll>(&cfg); + benchAll>(&cfg); + std::cout << geomean1(cfg) << std::endl; +} + +// 1773116 kb unordered_flat_set +// 2362932 kb unordered_node_set +// 4071776 kb std::unordered_set +TEST_CASE_TEMPLATE("memory_set_huge" * doctest::test_suite("bench") * doctest::skip(), Set, + robin_hood::unordered_flat_set, + robin_hood::unordered_node_set, std::unordered_set) { + Set set; + for (uint64_t n = 0; n < 80000000; ++n) { + set.insert(n); + } + std::cout << set.size() << std::endl; +} diff --git a/src/test/unit/unit_unordered_set.cpp b/src/test/unit/unit_unordered_set.cpp new file mode 100644 index 00000000..91b4356d --- /dev/null +++ b/src/test/unit/unit_unordered_set.cpp @@ -0,0 +1,27 @@ +#include + +#include + +#include + +TYPE_TO_STRING(robin_hood::unordered_flat_set); +TYPE_TO_STRING(robin_hood::unordered_node_set); + +TEST_CASE_TEMPLATE("unordered_set", Set, robin_hood::unordered_flat_set, + robin_hood::unordered_node_set) { + + REQUIRE(sizeof(typename Set::value_type) == sizeof(uint64_t)); + + Set set; + set.emplace(UINT64_C(123)); + REQUIRE(set.size() == 1U); + + set.insert(UINT64_C(333)); + REQUIRE(set.size() == 2U); + + set.erase(UINT64_C(222)); + REQUIRE(set.size() == 2U); + + set.erase(UINT64_C(123)); + REQUIRE(set.size() == 1U); +}