81

I understand the reasons why one can't just do this (rebalancing and stuff):

iterator i = m.find(33);

if (i != m.end())
  i->first = 22;

But so far the only way (I know about) to change the key is to remove the node from the tree alltogether and then insert the value back with a different key:

iterator i = m.find(33);

if (i != m.end())
{
  value = i->second;
  m.erase(i);
  m[22] = value;
}

This seems rather inefficient to me for more reasons:

  1. Traverses the tree three times (+ balance) instead of twice (+ balance)

  2. One more unnecessary copy of the value

  3. Unnecessary deallocation and then re-allocation of a node inside of the tree

I find the allocation and deallocation to be the worst from those three. Am I missing something or is there a more efficient way to do that?

I think, in theory, it should be possible, so I don't think changing for a different data structure is justified. Here is the pseudo algorithm I have in mind:

  1. Find the node in the tree whose key I want to change.

  2. Detach if from the tree (don't deallocate)

  3. Rebalance

  4. Change the key inside the detached node

  5. Insert the node back into the tree

  6. Rebalance

Donald Duck
  • 8,409
  • 22
  • 75
  • 99
Peter Jankuliak
  • 3,464
  • 1
  • 29
  • 40
  • Yup it is inefficient. Use a different data structure if this doesn't suite the use case – sehe Apr 21 '11 at 11:56
  • @sehe, I don't think that it's a problem with the data structure, if I was about to create my own I would end up with the same red-black tree with only difference that it would have a method which would _reuse_ the node instead of the allocation and reallocation. – Peter Jankuliak Apr 21 '11 at 12:04
  • "1.traverses the tree three times (+ balance) instead of twice (+ balance)" - it's twice instead of once... no traversal is necessary for `end()`. – Tony Delroy May 08 '13 at 01:28
  • @TonyDelroy I believe operator[] is the third one. – luizfls Jun 16 '20 at 22:20
  • @luizfls: there's traversal for `find`, then the `erase(i)` takes the iterator as argument (not the value to be erased) avoiding another traversal, but the final `insert` does need to do a second traversal to find the new place to insert at (as when the key changes, it'll potentially be at an unrelated position - if you know the new insertion should be very close in the tree to the old one, you can provide an insertion hint). – Tony Delroy Jun 18 '20 at 04:21
  • @TonyDelroy true, you are right! – luizfls Jun 18 '20 at 04:39

7 Answers7

89

In C++17, the new map::extract function lets you change the key.
Example:

std::map<int, std::string> m{ {10, "potato"}, {1, "banana"} };
auto nodeHandler = m.extract(10);
nodeHandler.key() = 2;
m.insert(std::move(nodeHandler)); // { { 1, "banana" }, { 2, "potato" } }
21koizyd
  • 1,843
  • 12
  • 25
34

You can omit the copying of value;

const int oldKey = 33;
const int newKey = 22;
const iterator it = m.find(oldKey);
if (it != m.end()) {
  // Swap value from oldKey to newKey, note that a default constructed value 
  // is created by operator[] if 'm' does not contain newKey.
  std::swap(m[newKey], it->second);
  // Erase old key-value from map
  m.erase(it);
}
Viktor Sehr
  • 12,825
  • 5
  • 58
  • 90
  • 1
    @Viktor, hmm, on second though, std::swap does two assignments and introduces a temporary value as well (by default). But _if (...) { m[22] = it->second; m.erase(it); }_ would probably do. – Peter Jankuliak Apr 21 '11 at 13:11
  • @Peter: If copying or assigning `value_type` is a performance issue, `std::swap` should be specialized to be *more* efficient than the single assignment in `m[22] = it->second;`. – aschepler Apr 21 '11 at 13:16
  • @Peter: if your value_type is such a simple type, omitting the copying is a nano-optimization in this context. – Viktor Sehr Apr 21 '11 at 13:17
  • @Viktor, true, I was thinking in a general case. – Peter Jankuliak Apr 21 '11 at 13:22
  • @aschepler, I know this is just a nit picking, but I believe that any specialization to std::swap would necessarily have to do (to be correct) one redundant assignment back to it->second. Any way, I agree with Viktors last note in that this extra asssignment was least troubling. – Peter Jankuliak Apr 21 '11 at 13:42
34

I proposed your algorithm for the associative containers about 18 months ago here:

http://www.open-std.org/jtc1/sc22/wg21/docs/lwg-closed.html#839

Look for the comment marked: [ 2009-09-19 Howard adds: ].

At the time, we were too close to FDIS to consider this change. However I think it very useful (and you apparently agree), and I would like to get it in to TR2. Perhaps you could help by finding and notifying your C++ National Body representative that this is a feature you would like to see.

Update

It is not certain, but I think there is a good chance we will see this feature in C++17! :-)

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • I'll try contacting him (not sure where to start though, but sure google will help). The story behind my question is that I've been developing such map for a company - which prefers not to use external libraries (embeded systems) - and the property of RB trees that they keep its elements sorted and fast key changing appeared to be crucial in some occasions. I wanted to keep the API same as in std::map but to my suprise found nothing that would solve this problem. So I'll try to implement what you proposed in the link, thanks. – Peter Jankuliak Apr 21 '11 at 15:54
  • 3
    Please feel free to contact me directly if you would like help identifying your NB representative. Note to everyone else: The odds of getting this into a standard are stacked against us if only two people support this. If you want to see this functionality added to the ordered and unordered associative containers, then lobby for it! Don't wait until 2016 and then complain when you find out it isn't there. **Now** is the time to act. – Howard Hinnant Apr 21 '11 at 16:01
  • @HowardHinnant, um, were you trying to get this into C++11? What about C++14? – ThomasMcLeod Oct 07 '13 at 16:31
  • @ThomasMcLeod: Tried and failed: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3645.pdf Maybe C++17... Without a popular uprising of impatient C++ programmers, it is a tough sell. Alan did an excellent job of it with N3645, and actually got pretty close, but no cigar. – Howard Hinnant Oct 07 '13 at 23:50
  • @HowardHinnant, it was a gallant effort I see. – ThomasMcLeod Oct 08 '13 at 00:20
  • @HowardHinnant couldn't you please explain while combination of `map::emplace_hint(map::erase(i), ...)` is not a analogue for `splice`? – Dewfy Mar 12 '16 at 10:46
  • 1
    @Dewfy: `erase` destructs an object in the map, and deallocates the node holding that object. `emplace_hint` allocates a new node and constructs a new object in that node from the args of `emplace_hint`. Each of these member functions is defined in isolation, and they have no way of knowing when they are used in the combination you specify. Here is the latest proposal on this issue, and a new revision will be out in a few weeks: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0083r1.pdf – Howard Hinnant Mar 12 '16 at 17:06
  • Now that we know more about C++17, the way to go is probably to use map::extract(), then change the key of the returned node handle and re-insert it, right? That should avoid copying over the contents. Any better ideas? – Lukas Barth Jan 11 '17 at 16:26
  • 1
    @LukasBarth: You are exactly right! And that will be as good as it gets. The extraction / insertion cycle will use the existing node, avoiding all allocation/deallocation (except any your key reassignment might do). – Howard Hinnant Jan 11 '17 at 19:52
8

Keys in STL maps are required to be immutable.

Seems like perhaps a different data structure or structures might make more sense if you have that much volatility on the key side of your pairings.

Joe
  • 41,484
  • 20
  • 104
  • 125
5

You cannot.

As you noticed, it is not possible. A map is organized so that you can change the value associated to a key efficiently, but not the reverse.

You have a look at Boost.MultiIndex, and notably its Emulating Standard Container sections. Boost.MultiIndex containers feature efficient update.

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
1

You should leave the allocation to the allocator. :-)

As you say, when the key changes there might be a lot of rebalancing. That's the way a tree works. Perhaps 22 is the first node in the tree and 33 the last? What do we know?

If avoiding allocations is important, perhaps you should try a vector or a deque? They allocate in larger chunks, so they save on number of calls to the allocator, but potentially waste memory instead. All the containers have their tradeoffs and it is up to you to decide which one has the primary advantage that you need in each case (assuming it matters at all).

For the adventurous:
If you know for sure that changing the key doesn't affect the order and you never, ever make a mistake, a little const_cast would let you change the key anyway.

Bo Persson
  • 90,663
  • 31
  • 146
  • 203
  • thanks for the input, but the question isn't really about which data structure to use. Rather the question is (if there is negative answer to my original one): why is there no API for doing it effectively if in theory there seems to be no reason why there shouldn't be. The pseudo algorithm is simple: find the node; detach it from the tree; rebalance; change the key in the detached node; insert back; rebalance; – Peter Jankuliak Apr 21 '11 at 12:20
  • @Peter - Perhaps re-keying isn't a fundamental operation on a map? I think it also saves the key from having to be assignable, which would let a few extra types be used as a key. – Bo Persson Apr 21 '11 at 12:35
0

If you know that the new key is valid for the map position (changing it wo't change the ordering), and you don't want the extra work of removing and adding the item to the map, you can use a const_cast to change the key, like in unsafeUpdateMapKeyInPlace below:

template <typename K, typename V, typename It>
bool isMapPositionValidForKey (const std::map<K, V>& m, It it, K key)
{
    if (it != m.begin() && std::prev (it)->first >= key)
        return false;
    ++it;
    return it == m.end() || it->first > key;
}

// Only for use when the key update doesn't change the map ordering
// (it is still greater than the previous key and lower than the next key).
template <typename K, typename V>
void unsafeUpdateMapKeyInPlace (const std::map<K, V>& m, typename std::map<K, V>::iterator& it, K newKey)
{
    assert (isMapPositionValidForKey (m, it, newKey));
    const_cast<K&> (it->first) = newKey;
}

If you want a solution that only changes in-place when that's valid, and otherwise changes the map structure:

template <typename K, typename V>
void updateMapKey (const std::map<K, V>& m, typename std::map<K, V>::iterator& it, K newKey)
{
    if (isMapPositionValidForKey (m, it, newKey))
    {
        unsafeUpdateMapKeyInPlace (m, it, newKey);
        return;
    }
    auto next = std::next (it);
    auto node = m.extract (it);
    node.key() = newKey;
    m.insert (next, std::move (node));
}
yairchu
  • 23,680
  • 7
  • 69
  • 109