Search code examples
c++operator-overloadingstdmapuser-defined-types

How to control and modify the std::map ordering using a user-defined key


I started out by using a std::string as my map key, as each item in my map can be uniquely identified by a string alone.

Then I realised that it would be a lot more useful to me to have the map ordered in a certain way, based on another parameter, so I added an int called priority to my key to help with ordering. The idea is that I iterate over the map and process the higher priority items first. I now have the following user-defined struct as my map key :

struct MyKey {

    // key data
    std::string addr;
    int priority;

    // constructor
    MyKey(const std::string & s, const int p) 
        : addr(s), priority(p) {}

    // overloaded operator
    bool operator<(const MyKey &that) const {

        // same key if addr is the same
        if (that->addr == this.addr)
            return false;

        // not same key so look at priorities to determine order
        if (that.priority < this->priority)
            return true;
        if (that.priority > this->priority)
            return false;

        // priorities are the same so use the string compare
        return (that.addr > this->addr);
    }
};

The map ordering appears to be working correctly, and when new items are added they are entered at the expected position automatically if you were to iterate over the map. For instance for a map of std::string values:

std::map<myKey, std::string> myMap;

myKey key1 = myKey(std::string("key1"), 1);
myKey key2 = myKey(std::string("key2"), 2);
myKey key3 = myKey(std::string("key3"), 3);
myKey key4 = myKey(std::string("key4"), 4);

myMap[key1] = std::string("value1");
myMap[key2] = std::string("value2");
myMap[key3] = std::string("value3");
myMap[key4] = std::string("value4");

Would result in the following map key-value pairs at respective indexes:

[0] { addr = "key4", priority = 4 }, { "value4" }
[1] { addr = "key3", priority = 3 }, { "value3" }
[2] { addr = "key2", priority = 2 }, { "value2" }
[3] { addr = "key1", priority = 1 }, { "value1" }

However...I am having problems when it comes to modifying an existing priority of a key that is already present in the map.

In this situation, find() and [] (with respect to std::map) don't work as I want them to:

myKey modified_key1 = myKey(std::string("key1"), 5);

// problem 1 - this does not return iterator to "key1", 
// but instead to end of the map
auto & foundKey = myMap.find(modified_key1);

// problem 2 - this adds a brand new item to the map
myMap[modified_key1] = std::string("value1");

After problem 2 as mentioned above, I am getting a new item added to the map with the same addr of an existing item. The new item appears to be added in the expected position based on the new (modified) priority, but the existing item to be updated remains as it was. So I end up with 2 items in the map with the same addr in their keys:

[0] { addr = "key1", priority = 5 }, { "value1" }
[1] { addr = "key4", priority = 4 }, { "value4" }
[2] { addr = "key3", priority = 3 }, { "value3" }
[3] { addr = "key2", priority = 2 }, { "value2" }
[4] { addr = "key1", priority = 1 }, { "value1" }

This is a problem for me as I would like to still rely on the notion that the addr of the map item key is unique.

What I want is the map to realise it already has an item with the same key (or more to the point the same key addr) and to re-order the item accordingly.

I have tried experimenting with compare functors as part of the map definition, and also overloading the keys == operator, but the same problem persists.

What am I missing or should I be approaching this differently?


Solution

  • The problem is that your comparison operator implemented incorrectly, it does not provide strict weak ordering hence undefined behavior of the std::map, lets say you have 3 objects of MyKey:

    MyKey mk1{ "a",3 }, mk2{ "b", 2 }, mk3 { "a", 1 };
    mk1 < mk2 -> true as 3 > 2
    mk2 < mk3 -> true as 2 > 1
    mk1 < mk3 -> false as addr is the same, but must be true
    

    live example

    I do not think your problem is easily solvable with std::map. Possible solution is to use boost::multi_index with address as one index and priority as another. To change priority of existing element boost::multi_index provides method to replace data.