UPDATE: Current best state of a solution incorporating feedback and further development is in this answer.
Review on this Design#2 please:
Simple wrapper class template for std::map
for the purpose of "retaining insertion order". This is quite a frequently asked question, here and here.
This is the follow on 2nd Design of a solution for this original question.
Code is still a bit rough, but it implements the new strategy:
- Basically a
std::map<KeyT,ValueT>
- But the
ValueT
is wrapped in a struct which hold prev/next pointers to make a doubly linked list - These pointers are maintained on insertion and deletion
- Thus rudimentary iteration in original insertion order is possible (to be improved -- input wanted on best way to do that)
#include <algorithm>
#include <iostream>
#include <list>
#include <map>
#include <random>
#include <string>
#include <vector>
template <class KeyT, class ValueT>
struct SequencedMapValue;
template <class KeyT, class ValueT>
class SequencedMap {
using MapT = std::map<KeyT, SequencedMapValue<KeyT, ValueT>>;
using MapItT = typename MapT::iterator;
using MapValT = typename MapT::value_type;
public:
template <class K, class V>
std::pair<MapItT, bool> insert_or_assign(const K& key, V&& value) {
const auto map_it = map.find(key);
if (map_it != map.end()) {
map_it->second.v = value;
return {map_it, false};
}
SequencedMapValue<KeyT, ValueT> s(std::forward<V>(value));
MapValT pair = std::make_pair(std::move(key), std::move(s));
const auto ins_res = map.insert(std::move(pair));
auto [elem_it, was_new] = ins_res;
if (tail) {
tail->second.next = &*elem_it;
elem_it->second.prev = tail;
tail = &*elem_it;
} else {
tail = &*elem_it;
head = tail;
}
return ins_res;
}
MapItT find(const KeyT& key) const { return map.find(key); }
ValueT& operator[](const KeyT& key) {
const auto map_it = map.find(key);
if (map_it == map.end())
throw std::logic_error(
"Warning! You are trying to create a SequencedMap entry using [] operator. Use "
"insert_or_assign for safety!");
return map_it->second.v;
}
MapItT erase(const KeyT& key) {
const auto map_it = map.find(key);
if (map_it != map.end()) {
// close gap in ptrs
if (!map_it->second.next) {
// last one
tail = map_it->second.prev;
map_it->second.prev->second.next = nullptr;
} else if (!map_it->second.prev) {
// this is head
head = map_it->second.next;
map_it->second.next->second.prev = nullptr;
} else {
// somewhere in the middle
map_it->second.prev->second.next = map_it->second.next;
map_it->second.next->second.prev = map_it->second.prev;
}
}
return map.erase(map_it);
}
const MapT& getMap() const { return map; }
MapValT* const ibegin() const { return head; }
const MapValT* const cibegin() const { return head; }
private:
MapT map;
MapValT* tail = nullptr;
MapValT* head = nullptr;
};
template <class KeyT, class ValueT>
struct SequencedMapValue {
using MapT = std::map<KeyT, SequencedMapValue<KeyT, ValueT>>;
using MapValT = typename MapT::value_type;
template <class V>
SequencedMapValue(V&& v_) : v{std::forward<V>(v_)} {}
ValueT v;
MapValT* next = nullptr;
MapValT* prev = nullptr;
};
// EOF class: Rest is demo usage code
template <class KeyT, class ValueT>
void print_in_insertion_order(const SequencedMap<KeyT, ValueT>& smap) {
auto curr = smap.ibegin();
while (curr) {
std::cout << curr->first << " -> " << curr->second.v << "\n";
curr = curr->second.next;
}
}
template <class KeyT, class ValueT>
void print_in_map_order(const SequencedMap<KeyT, ValueT>& smap) {
for (auto& pair: smap.getMap()) {
std::cout << pair.first << " -> " << pair.second.v << "\n";
}
}
int main() {
using Key = std::string;
using Value = int;
SequencedMap<Key, Value> smap;
// arbitrary ad-hoc temporary structure for the data (for demo purposes only)
std::cout << "insert data...\n";
for (auto p: std::vector<std::pair<Key, Value>>{
{"Mary", 10}, {"Alex", 20}, {"Johnny", 30}, {"Roman", 40}, {"Johnny", 50}}) {
smap.insert_or_assign(p.first, p.second);
}
print_in_insertion_order(smap);
std::cout << "\nsorted by key\n";
print_in_map_order(smap);
std::cout << "\nretrieve by known key\n";
auto key = "Alex";
std::cout << key << " -> " << smap["Alex"] << "\n";
std::cout << "\nchange value by known key: Johnny++\n";
++smap["Johnny"];
print_in_insertion_order(smap);
std::cout << "\ndelete by known key: Johnny\n";
smap.erase("Johnny");
print_in_insertion_order(smap);
}
I struggle in insert_or_assign()
with all the "universal references" and different template params apparently doing "the same thing". I sprinkled some std::move
and std::forward
, and made it compile and work, but I am sure it's not right.
I also struggled breaking the recursive template Type params and Type aliases between SequencedMap
and SequencedMapValue
. It works without static_cast
ing from void*
now, but there is probably a better way. I need help on how to do the iterators cleanly.
--
The benchmark (separate code) has been filled out more as well, and it looks good compared to the original design. No more slow deletes. Really on average about the same as std::map
(except, insertion ~35% slower). Compiled on clang-8 -std=C++17 -O3.
SequencedMap: insert 100,000=81.4451ms
SequencedMap: iterate in insertion order=0.844402ms
SequencedMap: Check sum=4990318
SequencedMap: modify 100,000 in insertion order=0.871902ms
SequencedMap: iterate in insertion order=0.792979ms
SequencedMap: Check sum=5090318
SequencedMap: delete 10,000=6.52532ms
SequencedMap: iterate in insertion order=0.83679ms
SequencedMap: Check sum=4581601
Map: insert 100,000=59.9917ms
Map: iterate in map order=3.19841ms
Map: Check sum=4990318
Map: modify 100,000 in map order=18.3977ms
Map: iterate in map order=3.66884ms
Map: Check sum=5090318
Map: delete 10,000=4.3003ms
Map: iterate in map order=2.59503ms
Map: Check sum=4581601
std::map
orstd::unordered_map
) which retains the order in which pairs were inserted; ie you can iterate it in the original insertion order -- this is not normally the case for std::(unordered)_map.. \$\endgroup\$