I have a need for a container that behaves like an unordered_map
, but has a fixed capacity, and an eviction policy where the least recently inserted key-value pair is chosen to be evicted when an insertion is performed at capacity.
I've implemented this by creating a container which combines a regular unordered_map
with a queue of iterators (implemented as a circular array). When a new item is inserted the iterator at the end of the queue is erased.
fixed_size_unordered_map
#include <array>
#include <unordered_map>
template<typename Key, typename U, std::size_t capacity>
class fixed_size_unordered_map {
using iterator = typename std::unordered_map<Key, U>::iterator;
using size_type = typename std::unordered_map<Key, U>::size_type;
using value_type = typename std::unordered_map<Key, U>::value_type;
public:
fixed_size_unordered_map() { iter_queue.fill(items.end()); }
fixed_size_unordered_map(const fixed_size_unordered_map&) = default;
fixed_size_unordered_map& operator=(const fixed_size_unordered_map&) = default;
fixed_size_unordered_map(fixed_size_unordered_map&&) = default;
fixed_size_unordered_map& operator=(fixed_size_unordered_map&&) = default;
size_type count(const Key& key) const {
return items.count(key);
}
const Key& at(const Key& key) const {
return items.at(key);
}
std::pair<iterator, bool> insert(const value_type &v) {
if(iter_queue[queue_idx] != items.end()){
items.erase(iter_queue[queue_idx]);
}
const auto ret = items.insert(v);
if(ret.second){
iter_queue[queue_idx] = ret.first;
queue_idx = (queue_idx + 1) % capacity;
}
return ret;
}
/* could implement more map<T, U> delegates as appropriate, the above are the ones
* I actually need for my use case */
protected:
std::unordered_map<Key, U> items;
std::array<iterator, capacity> iter_queue;
std::size_t queue_idx{0};
};
Example usage:
fixed_size_unordered_map<int, int, 10> m;
for(int i=0; i<=10; ++i){
m.insert(std::make_pair(i, 2*i*i));
}
std::cout << m.at(10) << "\t" << m.at(1) << "\t" << m.count(0) << std::endl;