4

I have looked around here and similar sites the past few days and have spent many hours trying to come up with a solution and would like to reach out for advice.

I have come to the disappointing conclusion that without going into boost libraries for C++ there is no possible way to create an associative container which retains an indexed ordering.

More clearly and specifically what I am in need of, is a map which can lookup using operator[key] but also is indexed in the order elements were added for iteration purposes.

I decided this morning I would need to write one myself, and I have tried a few approaches using maps of maps and vectors of pairs, etc. But nothing has actually worked and getting all of the functionality I am looking for is surprisingly not easily achievable whatsoever in this language. Ive got to be wrong right? Has anyone else had an experience with needing this functionality or familiar with this concept that could point me in the right direction of what I am looking for?

Many thanks in advance! Happy new year everyone.

  • Of course it's possible in the language - have you tried using a `Map` for your key lookups with a backing `Vector` with items inserted/removed in insertion order? – hnefatl Dec 30 '17 at 23:16
  • @KillzoneKid It surprisingly does not! That was the very first false solution that I had on this. – JosephJerrel Dec 30 '17 at 23:17
  • Have you looked at boost::multi_index? I'm not sure if it can do what you are after or not. – SoronelHaetir Dec 30 '17 at 23:18
  • @SoronelHaetir I did briefly look at it and I do believe much of what I am looking for can be found there but to be honest I have zero experience with an external library like boost and the syntax and construction for that multi_index object looked highly confusing to me. lol. I was hoping to find a simpler solution before tackling that education. – JosephJerrel Dec 30 '17 at 23:21
  • @hnefatl This sounds very interesting! could you please give me a slightly more detailed explanation of this model. A node is an object which knows its prev and next element correct? – JosephJerrel Dec 30 '17 at 23:23
  • Write the interface you want, then internally, keep two containers in parallel. One associative, one vector. If I understood correctly, you get no benefit from trying to combine them in one container, other than keeping them in sync, which your interface functions will handle. – Kenny Ostrom Dec 30 '17 at 23:42
  • 1
    @hnefatl Of course, my bad, don't even what was I thinking – Killzone Kid Dec 30 '17 at 23:45
  • @JosephJerrel -- *To be honest I have zero experience with an external library* -- The multi_index is a header-only library, meaning all you have to do is include the header(s), create the object, call the functions. No different than `#include `, declaring your vectors, and calling the vector functions. – PaulMcKenzie Dec 30 '17 at 23:49
  • @PaulMcKenzie not sure what I was looking at then because I thought I saw a heavily convoluted construction and use of that object and I kinda thought Id put it off until necessary to really examine it but I will definitely have to take another look! – JosephJerrel Dec 31 '17 at 00:06

1 Answers1

1

Warning: This is a very rough mock-up of the desired behaviour. This isn't anywhere near good code, but it's quick and should demonstrate a technique for doing this.

You should use existing solutions like Boost's multi_index before considering rolling your own. It'll be easier, faster, less error-prone, and better designed.

template<typename Key, typename Val>
class OrderedMap
{
private:
    std::vector<std::pair<Key, Val>> ordered;
    std::map<Key, std::size_t> lookup;

public:
    void insert(Key k, Val v)
    {
        ordered.push_back(std::pair<Key, Val>(k, v));
        lookup.emplace(k, ordered.size() - 1);
    }

    Val find(Key k)
    {
        std::size_t index = lookup[k];
        return ordered[index].second;
    }
    // "typename" needed as the "iterator" is a dependent type
    typename std::vector<std::pair<Key, Val>>::iterator begin()
    {
        return ordered.begin();
    }
    typename std::vector<std::pair<Key, Val>>::iterator end()
    {
        return ordered.end();
    }
};

All we need is a std::vector<std::pair<Key, Val>> to keep track of the actual order of elements that were inserted, and a std::map<Key, std::size_t> to keep track of the association between keys and value indices. Then we can delegate the functionality we want from this class to the functions of these internal backing/lookup structures.

The interface this class provides between the desired behaviour and the internal containers can be as robust as you please - here, I've only fleshed out the ugly bones needed for a demo.


See a quick demo here.

OrderedMap<std::string, int> m;
m.insert("1", 1);
m.insert("2", 2);
m.insert("3", 3);

std::cout << m.find("2") << std::endl << std::endl;

for (auto i = m.begin(); i != m.end(); i++)
    std::cout << i->first << " " << i->second << std::endl;
std::cout << std::endl;

Yields the output:

2

1 1
2 2
3 3
hnefatl
  • 5,860
  • 2
  • 27
  • 49
  • 1
    Better map into the vector's indices, because pointers will easily get invalidated. – juanchopanza Dec 30 '17 at 23:46
  • You may want just the value (copy) rather than a pointer to the value. (edit) yeah the index would be better, if it never changes. – Kenny Ostrom Dec 30 '17 at 23:46
  • Pointers will break when vector relocates, also you are storing keys twice. Maping into indices in a pure vector will make removals a trouble. That proves it ain't so easy to draft something sensible. There are some design decisions to be made. – luk32 Dec 30 '17 at 23:52
  • @luk32 Yeah, I just updated to returning copies rather than pointers. I'm not too worried about storing keys twice, as I've said this is a simple demonstration - I'd rather keep it quick to read and understand, and not worry about optimisations. – hnefatl Dec 30 '17 at 23:56
  • 1
    I really enjoy this idea very much! I will think this over and use this perspective to help me create my ordered map. I did not think of using a pointer to the real value being held in the vector. I think this will be difficult to implement a removal or modification to a value but I will take this into consideration, thank you very much – JosephJerrel Dec 31 '17 at 00:02
  • @hnefatl can I ask if there is a significance to using emplace rather than insert on the map? – JosephJerrel Dec 31 '17 at 00:10
  • @JosephJerrel Take a look at [this question](https://stackoverflow.com/questions/17172080/insert-vs-emplace-vs-operator-in-c-map). In this case, I just used it as it's easier to read. – hnefatl Dec 31 '17 at 00:11
  • @hnefatl also I see now that you have updated to not use pointers to the values in the vector. I am confused as to the use of size_t in the way you are using it. Can you clarify its use? – JosephJerrel Dec 31 '17 at 00:14
  • @JosephJerrel These are questions you can find answers to already on SO. I'm using `std::size_t` as it's a better practice than `int` for accessing `vector`s. Not best practice, but better. In any case, it implies that the value will be used as an index, that it's not just some arbitrary integer value. – hnefatl Dec 31 '17 at 00:16
  • @hnefatl I see now how the vector is being accessed. Thats what I was confused for a bit on how that size_t integer was being used. I do understand how it is prefered to an int for indexing though. and thank you very much again for you time and help – JosephJerrel Dec 31 '17 at 00:18