mirror of
https://github.com/martinus/unordered_dense.git
synced 2026-01-18 17:21:27 +01:00
Add key replacement and erase-and-shift-down methods to unordered_dense
No tests yet
This commit is contained in:
@@ -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,
|
||||
|
||||
23
README.md
23
README.md
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
165
test/bench/replace_key.cpp
Normal 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.
|
||||
}
|
||||
@@ -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
717
test/unit/replace_key.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user