Add key replacement and erase-and-shift-down methods to unordered_dense

No tests yet
This commit is contained in:
Martin Leitner-Ankerl
2025-10-10 08:06:35 +02:00
parent f8059d8273
commit cd32d8e467
7 changed files with 973 additions and 26 deletions

View File

@@ -12,6 +12,8 @@ Checks: >-
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
-fuchsia-default-arguments-declarations,
-fuchsia-overloaded-operator,
-fuchsia-default-arguments-calls,
-llvmlibc-implementation-in-namespace,
-modernize-use-constraints,
-fuchsia-trailing-return,
-llvmlibc-callee-namespace,

View File

@@ -27,10 +27,11 @@ Additionally, there are `ankerl::unordered_dense::segmented_map` and `ankerl::un
- [3.2.5. Automatic Fallback to `std::hash`](#325-automatic-fallback-to-stdhash)
- [3.2.6. Hash the Whole Memory](#326-hash-the-whole-memory)
- [3.3. Container API](#33-container-api)
- [3.3.1. `auto extract() && -> value_container_type`](#331-auto-extract----value_container_type)
- [3.3.2. `extract()` single Elements](#332-extract-single-elements)
- [3.3.3. `[[nodiscard]] auto values() const noexcept -> value_container_type const&`](#333-nodiscard-auto-values-const-noexcept---value_container_type-const)
- [3.3.4. `auto replace(value_container_type&& container)`](#334-auto-replacevalue_container_type-container)
- [3.3.1. `auto replace_key(iterator it, K&& new_key) -> std::pair<iterator, bool>`](#331-auto-replace_keyiterator-it-k-new_key---stdpairiterator-bool)
- [3.3.2. `auto extract() && -> value_container_type`](#332-auto-extract----value_container_type)
- [3.3.3. `extract()` single Elements](#333-extract-single-elements)
- [3.3.4. `[[nodiscard]] auto values() const noexcept -> value_container_type const&`](#334-nodiscard-auto-values-const-noexcept---value_container_type-const)
- [3.3.5. `auto replace(value_container_type&& container)`](#335-auto-replacevalue_container_type-container)
- [3.4. Custom Container Types](#34-custom-container-types)
- [3.5. Custom Bucket Types](#35-custom-bucket-types)
- [3.5.1. `ankerl::unordered_dense::bucket_type::standard`](#351-ankerlunordered_densebucket_typestandard)
@@ -254,11 +255,17 @@ struct custom_hash_unique_object_representation {
In addition to the standard `std::unordered_map` API (see https://en.cppreference.com/w/cpp/container/unordered_map) we have additional API that is somewhat similar to the node API, but leverages the fact that we're using a random access container internally:
#### 3.3.1. `auto extract() && -> value_container_type`
#### 3.3.1. `auto replace_key(iterator it, K&& new_key) -> std::pair<iterator, bool>`
Updates the key of an element in-place without changing its position in the underlying container. This operation maintains iterator and reference stability - all existing iterators and references remain valid after the update.
Note that this can also be used as an optimization for `unordered_dense::set` when you want to `erase` one element and then `insert` a new element, this should be quite a bit faster.
#### 3.3.2. `auto extract() && -> value_container_type`
Extracts the internally used container. `*this` is emptied.
#### 3.3.2. `extract()` single Elements
#### 3.3.3. `extract()` single Elements
Similar to `erase()` I have an API call `extract()`. It behaves exactly the same as `erase`, except that the return value is the moved element that is removed from the container:
@@ -268,11 +275,11 @@ Similar to `erase()` I have an API call `extract()`. It behaves exactly the same
Note that the `extract(key)` API returns an `std::optional<value_type>` that is empty when the key is not found.
#### 3.3.3. `[[nodiscard]] auto values() const noexcept -> value_container_type const&`
#### 3.3.4. `[[nodiscard]] auto values() const noexcept -> value_container_type const&`
Exposes the underlying values container.
#### 3.3.4. `auto replace(value_container_type&& container)`
#### 3.3.5. `auto replace(value_container_type&& container)`
Discards the internally held container and replaces it with the one passed. Non-unique elements are
removed, and the container will be partly reordered when non-unique elements are found.

View File

@@ -1079,6 +1079,17 @@ private:
at(m_buckets, place) = bucket;
}
void erase_and_shift_down(value_idx_type bucket_idx) {
// shift down until either empty or an element with correct spot is found
auto next_bucket_idx = next(bucket_idx);
while (at(m_buckets, next_bucket_idx).m_dist_and_fingerprint >= Bucket::dist_inc * 2) {
auto& next_bucket = at(m_buckets, next_bucket_idx);
at(m_buckets, bucket_idx) = {dist_dec(next_bucket.m_dist_and_fingerprint), next_bucket.m_value_idx};
bucket_idx = std::exchange(next_bucket_idx, next(next_bucket_idx));
}
at(m_buckets, bucket_idx) = {};
}
[[nodiscard]] static constexpr auto calc_num_buckets(uint8_t shifts) -> size_t {
return (std::min)(max_bucket_count(), size_t{1} << (64U - shifts));
}
@@ -1183,15 +1194,7 @@ private:
template <typename Op>
void do_erase(value_idx_type bucket_idx, Op handle_erased_value) {
auto const value_idx_to_remove = at(m_buckets, bucket_idx).m_value_idx;
// shift down until either empty or an element with correct spot is found
auto next_bucket_idx = next(bucket_idx);
while (at(m_buckets, next_bucket_idx).m_dist_and_fingerprint >= Bucket::dist_inc * 2) {
at(m_buckets, bucket_idx) = {dist_dec(at(m_buckets, next_bucket_idx).m_dist_and_fingerprint),
at(m_buckets, next_bucket_idx).m_value_idx};
bucket_idx = std::exchange(next_bucket_idx, next(next_bucket_idx));
}
at(m_buckets, bucket_idx) = {};
erase_and_shift_down(bucket_idx);
handle_erased_value(std::move(m_values[value_idx_to_remove]));
// update m_values
@@ -1201,9 +1204,7 @@ private:
val = std::move(m_values.back());
// update the values_idx of the moved entry. No need to play the info game, just look until we find the values_idx
auto mh = mixed_hash(get_key(val));
bucket_idx = bucket_idx_from_hash(mh);
bucket_idx = bucket_idx_from_hash(mixed_hash(get_key(val)));
auto const values_idx_back = static_cast<value_idx_type>(m_values.size() - 1);
while (values_idx_back != at(m_buckets, bucket_idx).m_value_idx) {
bucket_idx = next(bucket_idx);
@@ -1787,6 +1788,59 @@ public:
return do_try_emplace(std::forward<K>(key), std::forward<Args>(args)...).first;
}
// Replaces the key at the given iterator with new_key. This does not change any other data in the underlying table, so
// all iterators and references remain valid. However, this operation can fail if new_key already exists in the table.
// In that case, returns {iterator to the already existing new_key, false} and no change is made.
//
// In the case of a set, this effectively removes the old key and inserts the new key at the same spot, which is more
// efficient than removing the old key and inserting the new key because it avoids repositioning the last element.
template <typename K>
auto replace_key(iterator it, K&& new_key) -> std::pair<iterator, bool> {
auto const new_key_hash = mixed_hash(new_key);
// first, check if new_key already exists and return if so
auto dist_and_fingerprint = dist_and_fingerprint_from_hash(new_key_hash);
auto bucket_idx = bucket_idx_from_hash(new_key_hash);
while (dist_and_fingerprint <= at(m_buckets, bucket_idx).m_dist_and_fingerprint) {
auto const& bucket = at(m_buckets, bucket_idx);
if (dist_and_fingerprint == bucket.m_dist_and_fingerprint &&
m_equal(new_key, get_key(m_values[bucket.m_value_idx]))) {
return {begin() + static_cast<difference_type>(bucket.m_value_idx), false};
}
dist_and_fingerprint = dist_inc(dist_and_fingerprint);
bucket_idx = next(bucket_idx);
}
// const_cast is needed because iterator for the set is always const, so adding another get_key overload is not
// feasible.
auto& target_key = const_cast<key_type&>(get_key(*it));
auto const old_key_bucket_idx = bucket_idx_from_hash(mixed_hash(target_key));
// Replace the key before doing any bucket changes. If it throws, no harm done, we are still in a valid state as we
// have not modified any buckets yet.
target_key = std::forward<K>(new_key);
auto const value_idx = static_cast<value_idx_type>(it - begin());
// Find the bucket containing our value_idx. It's guaranteed we find it, so no other stopping condition needed.
bucket_idx = old_key_bucket_idx;
while (value_idx != at(m_buckets, bucket_idx).m_value_idx) {
bucket_idx = next(bucket_idx);
}
erase_and_shift_down(bucket_idx);
// place the new bucket
dist_and_fingerprint = dist_and_fingerprint_from_hash(new_key_hash);
bucket_idx = bucket_idx_from_hash(new_key_hash);
while (dist_and_fingerprint < at(m_buckets, bucket_idx).m_dist_and_fingerprint) {
dist_and_fingerprint = dist_inc(dist_and_fingerprint);
bucket_idx = next(bucket_idx);
}
place_and_shift_up({dist_and_fingerprint, value_idx}, bucket_idx);
return {it, true};
}
auto erase(iterator it) -> iterator {
auto hash = mixed_hash(get_key(*it));
auto bucket_idx = bucket_idx_from_hash(hash);

View File

@@ -58,7 +58,7 @@ class deque_set : public ankerl::unordered_dense::detail::
using base_t::base_t;
};
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage,misc-use-anonymous-namespace)
#define TEST_CASE_MAP(name, ...) \
TEST_CASE_TEMPLATE(name, \
map_t, \

165
test/bench/replace_key.cpp Normal file
View File

@@ -0,0 +1,165 @@
#include <app/doctest.h>
#include <app/print.h>
#include <sys/types.h>
#include <third-party/nanobench.h>
namespace {
template <typename T>
void randomize_key(ankerl::nanobench::Rng* rng, uint32_t n, T* key) {
// we limit ourselves to 32bit n
*key = static_cast<T>(rng->bounded(n));
}
void randomize_key(ankerl::nanobench::Rng* rng, uint32_t n, std::string* key) {
uint64_t k{};
randomize_key(rng, n, &k);
std::memcpy(key->data(), &k, sizeof(k));
}
auto create_initial_map(ankerl::nanobench::Rng& rng, uint32_t max_entries, uint32_t bound)
-> ankerl::unordered_dense::map<uint32_t, uint32_t> {
auto map = ankerl::unordered_dense::map<uint32_t, uint32_t>();
uint32_t i = 0;
while (map.size() < max_entries) {
map[rng.bounded(bound)] = i++;
}
return map;
}
auto create_initial_map_string(ankerl::nanobench::Rng& rng, std::string prototype_key, uint32_t max_entries, uint32_t bound)
-> ankerl::unordered_dense::map<std::string, uint32_t> {
auto map = ankerl::unordered_dense::map<std::string, uint32_t>();
uint32_t i = 0;
while (map.size() < max_entries) {
randomize_key(&rng, bound, &prototype_key);
map[prototype_key] = i++;
}
return map;
}
} // namespace
TEST_CASE("bench_replace_key" * doctest::test_suite("bench") * doctest::skip()) {
using namespace std::chrono_literals;
uint64_t const seed = 123;
uint32_t const max_entries_mask = 4096 - 1;
uint32_t const bound_mask = ((max_entries_mask + 1) * 4) - 1;
auto const min_epoch_time = 100ms;
// using replace_key, should be fast
auto rng = ankerl::nanobench::Rng(seed);
auto map_a = create_initial_map(rng, max_entries_mask + 1, bound_mask);
auto const map_size = static_cast<uint32_t>(map_a.size());
size_t num_replaces_a = 0;
size_t num_iters_a = 0;
ankerl::nanobench::Bench().minEpochTime(min_epoch_time).run("replace_key", [&] {
++num_iters_a;
auto const rand_num = rng();
auto const it_offset = static_cast<decltype(map_a)::difference_type>(rand_num & max_entries_mask);
auto const replacement_key = static_cast<uint32_t>((rand_num >> 32U) & bound_mask);
auto const it = map_a.begin() + it_offset;
if (map_a.replace_key(it, replacement_key).second) {
++num_replaces_a;
}
});
REQUIRE(map_a.size() == map_size);
// without replace_key, should be slower
rng = ankerl::nanobench::Rng(seed);
auto map_b = create_initial_map(rng, max_entries_mask + 1, bound_mask);
REQUIRE(map_b.size() == map_size);
size_t num_replaces_b = 0;
size_t num_iters_b = 0;
ankerl::nanobench::Bench().minEpochTime(min_epoch_time).run("erase & try_emplace", [&] {
++num_iters_b;
auto const rand_num = rng();
auto const it_offset = static_cast<decltype(map_b)::difference_type>(rand_num & max_entries_mask);
auto const replacement_key = static_cast<uint32_t>((rand_num >> 32U) & bound_mask);
auto const it = map_b.begin() + it_offset;
if (!map_b.contains(replacement_key)) {
++num_replaces_b;
auto const old_value = it->second;
map_b.erase(it);
map_b.try_emplace(replacement_key, old_value);
}
});
test::print("iters: {:10} {:10}\n", num_iters_a, num_iters_b);
test::print("replaces/iters: {:10.3f} {:10.3f}\n",
static_cast<double>(num_replaces_a) / static_cast<double>(num_iters_a),
static_cast<double>(num_replaces_b) / static_cast<double>(num_iters_b));
// can't compare maps for equality because the order in the maps are different, so the auto const it = map_b.begin() +
// it_offset; does not point to the same element in both benchmarks.
}
TEST_CASE("bench_replace_key_string" * doctest::test_suite("bench") * doctest::skip()) {
using namespace std::chrono_literals;
uint64_t const seed = 123;
uint32_t const max_entries_mask = 4096 - 1;
uint32_t const bound_mask = ((max_entries_mask + 1) * 4) - 1;
auto const min_epoch_time = 100ms;
// using replace_key, should be fast
auto rng = ankerl::nanobench::Rng(seed);
auto prototype_key = std::string(200, 'x');
auto map_a = create_initial_map_string(rng, prototype_key, max_entries_mask + 1, bound_mask);
auto const map_size = static_cast<uint32_t>(map_a.size());
size_t num_replaces_a = 0;
size_t num_iters_a = 0;
ankerl::nanobench::Bench().minEpochTime(min_epoch_time).run("replace_key", [&] {
++num_iters_a;
auto const rand_num = rng();
auto const it_offset = static_cast<decltype(map_a)::difference_type>(rand_num & max_entries_mask);
auto const it = map_a.begin() + it_offset;
randomize_key(&rng, bound_mask, &prototype_key);
if (map_a.replace_key(it, prototype_key).second) {
++num_replaces_a;
}
});
REQUIRE(map_a.size() == map_size);
// without replace_key, should be slower
rng = ankerl::nanobench::Rng(seed);
auto map_b = create_initial_map_string(rng, prototype_key, max_entries_mask + 1, bound_mask);
REQUIRE(map_b.size() == map_size);
size_t num_replaces_b = 0;
size_t num_iters_b = 0;
ankerl::nanobench::Bench().minEpochTime(min_epoch_time).run("erase & try_emplace", [&] {
++num_iters_b;
auto const rand_num = rng();
auto const it_offset = static_cast<decltype(map_b)::difference_type>(rand_num & max_entries_mask);
auto const it = map_b.begin() + it_offset;
randomize_key(&rng, bound_mask, &prototype_key);
if (!map_b.contains(prototype_key)) {
++num_replaces_b;
auto const old_value = it->second;
map_b.erase(it);
map_b.try_emplace(prototype_key, old_value);
}
});
test::print("iters: {:10} {:10}\n", num_iters_a, num_iters_b);
test::print("replaces/iters: {:10.3f} {:10.3f}\n",
static_cast<double>(num_replaces_a) / static_cast<double>(num_iters_a),
static_cast<double>(num_replaces_b) / static_cast<double>(num_iters_b));
// can't compare maps for equality because the order in the maps are different, so the auto const it = map_b.begin() +
// it_offset; does not point to the same element in both benchmarks.
}

View File

@@ -7,12 +7,13 @@ test_sources = [
'app/ui/progress_bar.cpp',
'app/unordered_dense.cpp',
'bench/swap.cpp',
'bench/show_allocations.cpp',
'bench/quick_overall_map.cpp',
'bench/game_of_life.cpp',
'bench/find_random.cpp',
'bench/copy.cpp',
'bench/find_random.cpp',
'bench/game_of_life.cpp',
'bench/quick_overall_map.cpp',
'bench/replace_key.cpp',
'bench/show_allocations.cpp',
'bench/swap.cpp',
'fuzz/run.cpp',
@@ -65,6 +66,7 @@ test_sources = [
'unit/pmr.cpp',
'unit/reentrant.cpp',
'unit/rehash.cpp',
'unit/replace_key.cpp',
'unit/replace.cpp',
'unit/reserve_and_assign.cpp',
'unit/reserve.cpp',

717
test/unit/replace_key.cpp Normal file
View File

@@ -0,0 +1,717 @@
#include <app/doctest.h>
#include <app/print.h>
#include <third-party/nanobench.h>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
// These tests were all automatically created with Claude Sonnet 4.5
// reviewed by Martin Leitner-Ankerl
TEST_CASE_MAP("replace_key_basic", int, int) {
auto map = map_t();
map[1] = 100;
map[2] = 200;
map[3] = 300;
auto it = map.find(2);
REQUIRE(it != map.end());
REQUIRE(it->first == 2);
REQUIRE(it->second == 200);
// Update key 2 to 5
auto [new_it, success] = map.replace_key(it, 5);
REQUIRE(success);
REQUIRE(new_it == it); // Same iterator
REQUIRE(new_it->first == 5);
REQUIRE(new_it->second == 200); // Value unchanged
REQUIRE(map.size() == 3);
// Verify old key is gone and new key exists
REQUIRE(map.find(2) == map.end());
REQUIRE(map.find(5) != map.end());
REQUIRE(map[5] == 200);
}
TEST_CASE_MAP("replace_key_duplicate_fails", int, int) {
auto map = map_t();
map[1] = 100;
map[2] = 200;
map[3] = 300;
auto it = map.find(2);
REQUIRE(it != map.end());
// Try to update key 2 to 3 (which already exists)
auto [new_it, success] = map.replace_key(it, 3);
REQUIRE_FALSE(success);
REQUIRE(new_it == map.find(3)); // Returns iterator to existing key
REQUIRE(new_it->second == 300); // Points to the existing element
// Original element should be unchanged
auto it2 = map.find(2);
REQUIRE(it2 != map.end());
REQUIRE(it2->second == 200);
REQUIRE(map.size() == 3);
}
TEST_CASE_MAP("replace_key_iterator_stability", int, int) {
auto map = map_t();
map[1] = 100;
map[2] = 200;
map[3] = 300;
// Get iterators to all elements
auto it1 = map.find(1);
auto it2 = map.find(2);
auto it3 = map.find(3);
// Update key 2 to 5
auto [new_it, success] = map.replace_key(it2, 5);
REQUIRE(success);
// All original iterators should still be valid
REQUIRE(it1->first == 1);
REQUIRE(it1->second == 100);
REQUIRE(it2->first == 5); // Updated key
REQUIRE(it2->second == 200);
REQUIRE(it3->first == 3);
REQUIRE(it3->second == 300);
// new_it should be same as it2
REQUIRE(new_it == it2);
}
TEST_CASE_MAP("replace_key_references_stability", int, int) {
auto map = map_t();
map[1] = 100;
map[2] = 200;
map[3] = 300;
// Get references to values
auto& val1 = map[1];
auto& val2 = map[2];
auto& val3 = map[3];
auto it2 = map.find(2);
auto [new_it, success] = map.replace_key(it2, 5);
REQUIRE(success);
// All references should still be valid
REQUIRE(val1 == 100);
REQUIRE(val2 == 200);
REQUIRE(val3 == 300);
// Modifying through old reference should work
val2 = 250;
REQUIRE(map[5] == 250);
}
TEST_CASE_MAP("replace_key_single_element", int, int) {
auto map = map_t();
map[1] = 100;
auto it = map.find(1);
auto [new_it, success] = map.replace_key(it, 10);
REQUIRE(success);
REQUIRE(new_it->first == 10);
REQUIRE(new_it->second == 100);
REQUIRE(map.size() == 1);
REQUIRE(map.find(1) == map.end());
REQUIRE(map.find(10) != map.end());
}
TEST_CASE_MAP("replace_key_strings", std::string, std::string) {
auto map = map_t();
map["foo"] = "bar";
map["hello"] = "world";
map["test"] = "value";
auto it = map.find("hello");
REQUIRE(it != map.end());
auto [new_it, success] = map.replace_key(it, "goodbye");
REQUIRE(success);
REQUIRE(new_it->first == "goodbye");
REQUIRE(new_it->second == "world");
REQUIRE(map.size() == 3);
REQUIRE(map.find("hello") == map.end());
REQUIRE(map.find("goodbye") != map.end());
REQUIRE(map["goodbye"] == "world");
}
TEST_CASE_MAP("replace_key_move_semantics", std::string, int) {
auto map = map_t();
map["key1"] = 1;
map["key2"] = 2;
auto it = map.find("key1");
std::string new_key = "moved_key";
auto [new_it, success] = map.replace_key(it, std::move(new_key));
REQUIRE(success);
REQUIRE(new_it->first == "moved_key");
REQUIRE(new_it->second == 1);
REQUIRE(map.size() == 2);
}
TEST_CASE_MAP("replace_key_same_key", int, int) {
auto map = map_t();
map[1] = 100;
map[2] = 200;
auto it = map.find(1);
// Try to update key to itself
auto [new_it, success] = map.replace_key(it, 1);
// This should fail because key 1 already exists
REQUIRE_FALSE(success);
REQUIRE(new_it == map.find(1));
REQUIRE(map.size() == 2);
}
TEST_CASE_MAP("replace_key_multiple_updates", int, int) {
auto map = map_t();
map[1] = 100;
auto it = map.find(1);
// First update: 1 -> 2
auto [it1, s1] = map.replace_key(it, 2);
REQUIRE(s1);
REQUIRE(it1->first == 2);
// Second update: 2 -> 3
auto [it2, s2] = map.replace_key(it1, 3);
REQUIRE(s2);
REQUIRE(it2->first == 3);
// Third update: 3 -> 4
auto [it3, s3] = map.replace_key(it2, 4);
REQUIRE(s3);
REQUIRE(it3->first == 4);
REQUIRE(it3->second == 100);
REQUIRE(map.size() == 1);
REQUIRE(map.find(1) == map.end());
REQUIRE(map.find(2) == map.end());
REQUIRE(map.find(3) == map.end());
REQUIRE(map[4] == 100);
}
TEST_CASE_MAP("replace_key_large_map", int, int) {
auto map = map_t();
// Insert many elements
for (int i = 0; i < 1000; ++i) {
map[i] = i * 10;
}
// Update middle element
auto it = map.find(500);
REQUIRE(it != map.end());
auto [new_it, success] = map.replace_key(it, 10000);
REQUIRE(success);
REQUIRE(new_it->first == 10000);
REQUIRE(new_it->second == 5000);
REQUIRE(map.size() == 1000);
REQUIRE(map.find(500) == map.end());
REQUIRE(map.find(10000) != map.end());
// Verify all other elements are still accessible
for (int i = 0; i < 1000; ++i) {
if (i != 500) {
REQUIRE(map.find(i) != map.end());
REQUIRE(map[i] == i * 10);
}
}
}
TEST_CASE_MAP("replace_key_begin_iterator", int, int) {
auto map = map_t();
map[1] = 100;
map[2] = 200;
map[3] = 300;
auto it = map.begin();
int const old_key = it->first;
int value = it->second;
auto [new_it, success] = map.replace_key(it, 999);
REQUIRE(success);
REQUIRE(new_it->first == 999);
REQUIRE(new_it->second == value);
REQUIRE(map.size() == 3);
REQUIRE(map.find(old_key) == map.end());
REQUIRE(map.find(999) != map.end());
}
TEST_CASE_MAP("replace_key_end_minus_one", int, int) {
auto map = map_t();
map[1] = 100;
map[2] = 200;
map[3] = 300;
// Get last element (order is implementation-dependent, but we can get it)
auto it = map.begin();
std::advance(it, map.size() - 1);
int const old_key = it->first;
int value = it->second;
auto [new_it, success] = map.replace_key(it, 888);
REQUIRE(success);
REQUIRE(new_it->first == 888);
REQUIRE(new_it->second == value);
REQUIRE(map.size() == 3);
REQUIRE(map.find(old_key) == map.end());
}
TEST_CASE_MAP("replace_key_collision_chain", int, int) {
auto map = map_t();
// Insert elements that might collide
for (int i = 0; i < 100; ++i) {
map[i] = i * 2;
}
// Update an element in the middle
auto it = map.find(50);
REQUIRE(it != map.end());
auto [new_it, success] = map.replace_key(it, 5000);
REQUIRE(success);
REQUIRE(new_it->first == 5000);
REQUIRE(new_it->second == 100);
// Verify all elements still accessible
for (int i = 0; i < 100; ++i) {
if (i == 50) {
REQUIRE(map.find(i) == map.end());
} else {
REQUIRE(map.find(i) != map.end());
REQUIRE(map[i] == i * 2);
}
}
REQUIRE(map[5000] == 100);
}
TEST_CASE_MAP("replace_key_after_rehash", int, int) {
auto map = map_t();
// Insert elements to trigger potential rehash
for (int i = 0; i < 10; ++i) {
map[i] = i;
}
map.reserve(1000); // Force rehash
auto it = map.find(5);
REQUIRE(it != map.end());
auto [new_it, success] = map.replace_key(it, 555);
REQUIRE(success);
REQUIRE(new_it->first == 555);
REQUIRE(new_it->second == 5);
REQUIRE(map.find(5) == map.end());
REQUIRE(map.find(555) != map.end());
}
TEST_CASE_MAP("replace_key_preserve_value_modifications", int, int) {
auto map = map_t();
map[1] = 100;
map[2] = 200;
auto it = map.find(1);
it->second = 999; // Modify value before updating key
auto [new_it, success] = map.replace_key(it, 10);
REQUIRE(success);
REQUIRE(new_it->first == 10);
REQUIRE(new_it->second == 999); // Modified value should be preserved
}
TEST_CASE_MAP("replace_key_duplicate_with_different_value", int, int) {
auto map = map_t();
map[1] = 100;
map[2] = 200;
auto it = map.find(1);
// Try to update to existing key 2
auto [new_it, success] = map.replace_key(it, 2);
REQUIRE_FALSE(success);
REQUIRE(new_it == map.find(2));
REQUIRE(new_it->first == 2);
REQUIRE(new_it->second == 200); // Returns the existing element's value
// Original element unchanged
auto orig_it = map.find(1);
REQUIRE(orig_it != map.end());
REQUIRE(orig_it->second == 100);
}
TEST_CASE_MAP("replace_key_stress_test", int, int) {
auto map = map_t();
// Build initial map
for (int i = 0; i < 500; ++i) {
map[i] = i * 3;
}
// Update every 5th element
for (int i = 0; i < 500; i += 5) {
auto it = map.find(i);
if (it != map.end()) {
int new_key = i + 5000;
auto [new_it, success] = map.replace_key(it, new_key);
REQUIRE(success);
REQUIRE(new_it->first == new_key);
REQUIRE(new_it->second == i * 3);
}
}
// Verify final state
REQUIRE(map.size() == 500);
for (int i = 0; i < 500; ++i) {
if (i % 5 == 0) {
REQUIRE(map.find(i) == map.end());
REQUIRE(map.find(i + 5000) != map.end());
REQUIRE(map[i + 5000] == i * 3);
} else {
REQUIRE(map.find(i) != map.end());
REQUIRE(map[i] == i * 3);
}
}
}
TEST_CASE_MAP("replace_key_all_elements_sequentially", int, int) {
auto map = map_t();
// Insert 20 elements
for (int i = 0; i < 20; ++i) {
map[i] = i * 100;
}
// Update all keys by adding 1000
std::vector<std::pair<int, int>> elements;
for (auto& [k, v] : map) {
elements.push_back({k, v});
}
for (auto [old_key, value] : elements) {
auto it = map.find(old_key);
if (it != map.end()) {
auto [new_it, success] = map.replace_key(it, old_key + 1000);
REQUIRE(success);
}
}
// Verify all keys are updated
REQUIRE(map.size() == 20);
for (int i = 0; i < 20; ++i) {
REQUIRE(map.find(i) == map.end());
REQUIRE(map.find(i + 1000) != map.end());
REQUIRE(map[i + 1000] == i * 100);
}
}
// SET TESTS - replace_key works the same for sets as for maps
TEST_CASE_SET("replace_key_set_basic", int) {
auto set = set_t();
set.insert(1);
set.insert(2);
set.insert(3);
auto it = set.find(2);
REQUIRE(it != set.end());
REQUIRE(*it == 2);
// Update key 2 to 5
auto [new_it, success] = set.replace_key(it, 5);
REQUIRE(success);
REQUIRE(new_it == it); // Same iterator
REQUIRE(*new_it == 5);
REQUIRE(set.size() == 3);
// Verify old key is gone and new key exists
REQUIRE(set.find(2) == set.end());
REQUIRE(set.find(5) != set.end());
REQUIRE(set.contains(5));
REQUIRE_FALSE(set.contains(2));
}
TEST_CASE_SET("replace_key_set_duplicate_fails", int) {
auto set = set_t();
set.insert(1);
set.insert(2);
set.insert(3);
auto it = set.find(2);
REQUIRE(it != set.end());
// Try to update key 2 to 3 (which already exists)
auto [new_it, success] = set.replace_key(it, 3);
REQUIRE_FALSE(success);
REQUIRE(new_it == set.find(3)); // Returns iterator to existing key
REQUIRE(*new_it == 3);
// Original element should be unchanged
auto it2 = set.find(2);
REQUIRE(it2 != set.end());
REQUIRE(*it2 == 2);
REQUIRE(set.size() == 3);
}
TEST_CASE_SET("replace_key_set_iterator_stability", int) {
auto set = set_t();
set.insert(1);
set.insert(2);
set.insert(3);
// Get iterators to all elements
auto it1 = set.find(1);
auto it2 = set.find(2);
auto it3 = set.find(3);
// Update key 2 to 5
auto [new_it, success] = set.replace_key(it2, 5);
REQUIRE(success);
// All original iterators should still be valid
REQUIRE(*it1 == 1);
REQUIRE(*it2 == 5); // Updated key
REQUIRE(*it3 == 3);
// new_it should be same as it2
REQUIRE(new_it == it2);
}
TEST_CASE_SET("replace_key_set_strings", std::string) {
auto set = set_t();
set.insert("foo");
set.insert("hello");
set.insert("test");
auto it = set.find("hello");
REQUIRE(it != set.end());
auto [new_it, success] = set.replace_key(it, "goodbye");
REQUIRE(success);
REQUIRE(*new_it == "goodbye");
REQUIRE(set.size() == 3);
REQUIRE(set.find("hello") == set.end());
REQUIRE(set.find("goodbye") != set.end());
REQUIRE(set.contains("goodbye"));
}
TEST_CASE_SET("replace_key_set_single_element", int) {
auto set = set_t();
set.insert(42);
auto it = set.find(42);
auto [new_it, success] = set.replace_key(it, 999);
REQUIRE(success);
REQUIRE(*new_it == 999);
REQUIRE(set.size() == 1);
REQUIRE(set.find(42) == set.end());
REQUIRE(set.find(999) != set.end());
}
TEST_CASE_SET("replace_key_set_same_key", int) {
auto set = set_t();
set.insert(1);
set.insert(2);
auto it = set.find(1);
// Try to update key to itself
auto [new_it, success] = set.replace_key(it, 1);
// This should fail because key 1 already exists
REQUIRE_FALSE(success);
REQUIRE(new_it == set.find(1));
REQUIRE(set.size() == 2);
}
TEST_CASE_SET("replace_key_set_multiple_updates", int) {
auto set = set_t();
set.insert(1);
auto it = set.find(1);
// First update: 1 -> 2
auto [it1, s1] = set.replace_key(it, 2);
REQUIRE(s1);
REQUIRE(*it1 == 2);
// Second update: 2 -> 3
auto [it2, s2] = set.replace_key(it1, 3);
REQUIRE(s2);
REQUIRE(*it2 == 3);
// Third update: 3 -> 4
auto [it3, s3] = set.replace_key(it2, 4);
REQUIRE(s3);
REQUIRE(*it3 == 4);
REQUIRE(set.size() == 1);
REQUIRE(set.find(1) == set.end());
REQUIRE(set.find(2) == set.end());
REQUIRE(set.find(3) == set.end());
REQUIRE(set.contains(4));
}
TEST_CASE_SET("replace_key_set_large", int) {
auto set = set_t();
// Insert many elements
for (int i = 0; i < 1000; ++i) {
set.insert(i);
}
// Update middle element
auto it = set.find(500);
REQUIRE(it != set.end());
auto [new_it, success] = set.replace_key(it, 10000);
REQUIRE(success);
REQUIRE(*new_it == 10000);
REQUIRE(set.size() == 1000);
REQUIRE(set.find(500) == set.end());
REQUIRE(set.find(10000) != set.end());
// Verify all other elements are still accessible
for (int i = 0; i < 1000; ++i) {
if (i != 500) {
REQUIRE(set.contains(i));
}
}
}
TEST_CASE_SET("replace_key_set_begin_iterator", int) {
auto set = set_t();
set.insert(1);
set.insert(2);
set.insert(3);
auto it = set.begin();
int const old_value = *it;
auto [new_it, success] = set.replace_key(it, 999);
REQUIRE(success);
REQUIRE(*new_it == 999);
REQUIRE(set.size() == 3);
REQUIRE(set.find(old_value) == set.end());
REQUIRE(set.find(999) != set.end());
}
TEST_CASE_SET("replace_key_set_stress_test", int) {
auto set = set_t();
// Build initial set
for (int i = 0; i < 500; ++i) {
set.insert(i);
}
// Update every 5th element
for (int i = 0; i < 500; i += 5) {
auto it = set.find(i);
if (it != set.end()) {
int new_key = i + 5000;
auto [new_it, success] = set.replace_key(it, new_key);
REQUIRE(success);
REQUIRE(*new_it == new_key);
}
}
// Verify final state
REQUIRE(set.size() == 500);
for (int i = 0; i < 500; ++i) {
if (i % 5 == 0) {
REQUIRE_FALSE(set.contains(i));
REQUIRE(set.contains(i + 5000));
} else {
REQUIRE(set.contains(i));
}
}
}
TEST_CASE_SET("replace_key_set_move_semantics", std::string) {
auto set = set_t();
set.insert("key1");
set.insert("key2");
auto it = set.find("key1");
std::string new_key = "moved_key";
auto [new_it, success] = set.replace_key(it, std::move(new_key));
REQUIRE(success);
REQUIRE(*new_it == "moved_key");
REQUIRE(set.size() == 2);
REQUIRE_FALSE(set.contains("key1"));
REQUIRE(set.contains("moved_key"));
}
TEST_CASE_MAP("replace_key_random", uint32_t, uint32_t) {
auto map = map_t();
auto comparison_map = std::unordered_map<uint32_t, uint32_t>();
uint32_t idx = 0;
// inserts an element, and updates a random element in the map
auto rng = ankerl::nanobench::Rng();
while (idx < 10000) {
map[idx] = idx;
comparison_map[idx] = idx;
++idx;
auto const rng_idx = rng.bounded(idx);
auto const map_it = map.find(rng_idx);
auto const comparison_it = comparison_map.find(rng_idx);
if (map_it == map.end()) {
REQUIRE(comparison_it == comparison_map.end());
continue;
}
REQUIRE(comparison_it != comparison_map.end());
// test::print("map.replace_key(it, {})\n", idx);
auto const replacement_idx = rng.bounded(idx * 2);
auto const [new_it, success] = map.replace_key(map_it, replacement_idx);
// test::print(
// "auto [{:5}, {:5}] = map.replace_key({:5}, {:5})\n", (new_it - map.begin()), success, rng_idx,
// replacement_idx);
if (success) {
REQUIRE(comparison_map.end() == comparison_map.find(replacement_idx));
auto const val = comparison_it->second;
comparison_map.erase(comparison_it);
REQUIRE(comparison_map.try_emplace(replacement_idx, val).second);
} else {
auto const replacement_it = comparison_map.find(replacement_idx);
REQUIRE(replacement_it != comparison_map.end());
// make sure both iterators hold the same element
REQUIRE(replacement_it->first == new_it->first);
REQUIRE(replacement_it->second == new_it->second);
}
}
// now both map and comparison_map should hold the same key-value pairs
REQUIRE(map.size() == comparison_map.size());
for (auto const& [k, v] : comparison_map) {
auto it = map.find(k);
REQUIRE(it != map.end());
REQUIRE(it->second == v);
}
}