The ordered-map library provides a hash map and a hash set which preserve the order of insertion in a way similar to Python's OrderedDict. When iterating over the map, the values will be returned in the same order as they were inserted.
The values are stored contiguously in an underlying structure, no holes in-between values even after an erase operation. By default a std::deque
is used for this structure, but it's also possible to use a std::vector
. This structure is directly accessible through the values_container()
method and if the structure is a std::vector
, a data()
method is also provided to easily interact with C APIs. This provides fast iteration but with the drawback of an O(bucket_count) erase operation. An O(1) pop_back()
and an O(1) unordered_erase()
functions are available. If ordered erase is often used, another data structure is recommended.
To resolve collisions on hashes, the library uses linear robin hood probing with backward shift deletion.
The library provides a behaviour similar to a std::deque/std::vector
with unique values but with an average time complexity of O(1) for lookups and an amortised time complexity of O(1) for insertions. This comes at the price of a little higher memory footprint (8 bytes per bucket by default).
Two classes are provided: tsl::ordered_map
and tsl::ordered_set
.
Note: The library uses a power of two for the size of its buckets array to take advantage of the fast modulo. For good performances, it requires the hash table to have a well-distributed hash function. If you encounter performance issues check your hash function.
tsl::ordered_map
exported target from the CMakeLists.txt.std::unordered_map
but with faster insertions and reduced memory usage (see benchmark for details).find
with a type different than Key
(e.g. if you have a map that uses std::unique_ptr<foo>
as key, you can use a foo*
or a std::uintptr_t
as key parameter to find
without constructing a std::unique_ptr<foo>
, see example).precalculated_hash
parameter in API).serialize/deserialize
methods in the API for details).-fno-exceptions
option on Clang and GCC, without an /EH
option on MSVC or simply by defining TSL_NO_EXCEPTIONS
). std::terminate
is used in replacement of the throw
instruction when exceptions are disabled.std::unordered_map
and std::unordered_set
.std::unordered_map
tsl::ordered_map
tries to have an interface similar to std::unordered_map
, but some differences exist.
RandomAccessIterator
.std::vector
and std::deque
(see API for details). If you use std::vector
as ValueTypeContainer
, you can use reserve()
to preallocate some space and avoid the invalidation of the iterators on insert.erase()
operation, it has a complexity of O(bucket_count). A faster O(1) version unordered_erase()
exists, but it breaks the insertion order (see API for details). An O(1) pop_back()
is also available.operator==
and operator!=
are order dependent. Two tsl::ordered_map
with the same values but inserted in a different order don't compare equal.operator*()
and operator->()
return a reference and a pointer to const std::pair<Key, T>
instead of std::pair<const Key, T>
making the value T
not modifiable. To modify the value you have to call the value()
method of the iterator to get a mutable reference. Example:tsl::ordered_map<int, int> map = {{1, 1}, {2, 1}, {3, 1}};
for(auto it = map.begin(); it != map.end(); ++it) {
//it->second = 2; // Illegal
it.value() = 2; // Ok
}
IndexType
class template parameter, check the API for details.bucket_size
, bucket
, ...).Thread-safety guarantee is the same as std::unordered_map
(i.e. possible to have multiple concurrent readers with no writer).
These differences also apply between std::unordered_set
and tsl::ordered_set
.
If not mentioned otherwise, functions have the strong exception guarantee, see details. We below list cases in which this guarantee is not provided.
The guarantee is only provided if ValueContainer::emplace_back
has the strong exception guarantee (which is true for std::vector
and std::deque
as long as the type T
is not a move-only type with a move constructor that may throw an exception, see details).
The tsl::ordered_map::erase_if
and tsl::ordered_set::erase_if
functions only have the guarantee under the preconditions listed in their documentation.
To use ordered-map, just add the include directory to your include path. It is a header-only library.
If you use CMake, you can also use the tsl::ordered_map
exported target from the CMakeLists.txt with target_link_libraries
.
# Example where the ordered-map project is stored in a third-party directory
add_subdirectory(third-party/ordered-map)
target_link_libraries(your_target PRIVATE tsl::ordered_map)
If the project has been installed through make install
, you can also use find_package(tsl-ordered-map REQUIRED)
instead of add_subdirectory
.
The code should work with any C++11 standard-compliant compiler and has been tested with GCC 4.8.4, Clang 3.5.0 and Visual Studio 2015.
To run the tests you will need the Boost Test library and CMake.
git clone https://github.com/Tessil/ordered-map.git
cd ordered-map/tests
mkdir build
cd build
cmake ..
cmake --build .
./tsl_ordered_map_tests
The API can be found here.
#include <iostream>
#include <string>
#include <cstdlib>
#include <tsl/ordered_map.h>
#include <tsl/ordered_set.h>
int main() {
tsl::ordered_map<char, int> map = {{'d', 1}, {'a', 2}, {'g', 3}};
map.insert({'b', 4});
map['h'] = 5;
map['e'] = 6;
map.erase('a');
// {d, 1} {g, 3} {b, 4} {h, 5} {e, 6}
for(const auto& key_value : map) {
std::cout << "{" << key_value.first << ", " << key_value.second << "}" << std::endl;
}
map.unordered_erase('b');
// Break order: {d, 1} {g, 3} {e, 6} {h, 5}
for(const auto& key_value : map) {
std::cout << "{" << key_value.first << ", " << key_value.second << "}" << std::endl;
}
for(auto it = map.begin(); it != map.end(); ++it) {
//it->second += 2; // Not valid.
it.value() += 2;
}
if(map.find('d') != map.end()) {
std::cout << "Found 'd'." << std::endl;
}
const std::size_t precalculated_hash = std::hash<char>()('d');
// If we already know the hash beforehand, we can pass it as argument to speed-up the lookup.
if(map.find('d', precalculated_hash) != map.end()) {
std::cout << "Found 'd' with hash " << precalculated_hash << "." << std::endl;
}
tsl::ordered_set<char, std::hash<char>, std::equal_to<char>,
std::allocator<char>, std::vector<char>> set;
set.reserve(6);
set = {'3', '4', '9', '2'};
set.erase('2');
set.insert('1');
set.insert('