martinus / unordered_dense

A fast & densely stored hashmap and hashset based on robin-hood backward shift deletion
MIT License
898 stars 72 forks source link

change_key(itr, key) function #116

Open Auburn opened 6 months ago

Auburn commented 6 months ago

For my specific use case I require being able to change the key for a given element without changing it's index in the underlying array. As far as I can tell this isn't possible with the current API.

So I wrote a change_key function which takes the iterator of the element you want to change and a new key for it. If the new key already exists it doesn't make any change and returns the existing new key iterator, otherwise it returns the now updated iterator with it's new key.

I amalgamated the function from sections of emplace and erase, I wrote a few tests for it and it seems to work correctly. No idea if it's ideal optimisation wise but this is what it looks like

    template <typename Q = T, std::enable_if_t<is_map_v<Q>, bool> = true>
    auto change_key(iterator it, Key const& new_key) -> std::pair<iterator, bool>
    {
        auto const value_idx_to_change = static_cast<value_idx_type>(it - cbegin());

        auto new_hash = mixed_hash(new_key);
        auto new_bucket_idx = bucket_idx_from_hash( new_hash );
        auto dist_and_fingerprint = dist_and_fingerprint_from_hash( new_hash );

        while (true) {
            auto* bucket = &at(m_buckets, new_bucket_idx);
            if (dist_and_fingerprint == bucket->m_dist_and_fingerprint) {
                if (m_equal(new_key, get_key(m_values[bucket->m_value_idx]))) {
                    return {begin() + static_cast<difference_type>(bucket->m_value_idx), false};
                }
            } else if (dist_and_fingerprint > bucket->m_dist_and_fingerprint) {
                // remove old key from buckets
                auto old_hash = mixed_hash(get_key(*it));
                auto old_bucket_idx = bucket_idx_from_hash(old_hash);

                while(at(m_buckets, old_bucket_idx).m_value_idx != value_idx_to_change)
                {
                    old_bucket_idx = next(old_bucket_idx);
                }

                // shift down until either empty or an element with correct spot is found
                auto next_bucket_idx = next(old_bucket_idx);
                while(at(m_buckets, next_bucket_idx).m_dist_and_fingerprint >= Bucket::dist_inc * 2)
                {
                    at(m_buckets, old_bucket_idx) = {dist_dec(at(m_buckets, next_bucket_idx).m_dist_and_fingerprint),
                                                 at(m_buckets, next_bucket_idx).m_value_idx};
                    old_bucket_idx = std::exchange(next_bucket_idx, next(next_bucket_idx));
                }
                at(m_buckets, old_bucket_idx) = {};                

                // place new key, with existing value idx
                place_and_shift_up({dist_and_fingerprint, value_idx_to_change}, new_bucket_idx);
                it->first = new_key;
                return {it, true};
            }
            dist_and_fingerprint = dist_inc(dist_and_fingerprint);
            new_bucket_idx = next(new_bucket_idx);
        }           
    }

Basically I wanted to know if the function implementation looks sensible to you. You are free to add it to the project if if you think it would be useful. Thanks