diff --git a/include/nlohmann/ordered_map.hpp b/include/nlohmann/ordered_map.hpp index 2a293fc93e..85afd9f70c 100644 --- a/include/nlohmann/ordered_map.hpp +++ b/include/nlohmann/ordered_map.hpp @@ -8,352 +8,820 @@ #pragma once -#include // equal_to, less -#include // initializer_list -#include // input_iterator_tag, iterator_traits -#include // allocator -#include // for out_of_range -#include // enable_if, is_convertible -#include // pair -#include // vector +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include NLOHMANN_JSON_NAMESPACE_BEGIN - -/// ordered_map: a minimal map-like container that preserves insertion order -/// for use within nlohmann::basic_json -template , - class Allocator = std::allocator>> - struct ordered_map : std::vector, Allocator> +/*! +@brief Insertion-order-preserving map with O(1) average lookup + +Internally uses a std::list to contain the pairs, plus +an index (effectively keyed by a pointer-to-key) for O(1) lookups. + +This implements all the methods of std::map except: +- Ordering-dependent operations (lower_bound, upper_bound, equal_range, + key_comp and value_comp, etc.); these simply make no sense without + lexicographic ordering. +- Extensions added after C++11 (try_emplace, insert_or_assign, contains, + erase_if, etc.). + +It also has iterator arithmetic (it+n), which is not used by the library +but needed by some unit tests of the original implementation. + +Stable iterators guarantee: insertion and value updates do not invalidate +iterators (except for the iterator to an element that is erased). + +Size complexity is linear to the size of contained data; time complexity +is on average O(1) for all access methods and all methods that change a +single element, O(N) when N elements are affected. +*/ +template, // Unused + class Allocator = std::allocator>> + struct ordered_map { - using key_type = Key; - using mapped_type = T; - using Container = std::vector, Allocator>; - using iterator = typename Container::iterator; - using const_iterator = typename Container::const_iterator; - using size_type = typename Container::size_type; - using value_type = typename Container::value_type; -#ifdef JSON_HAS_CPP_14 - using key_compare = std::equal_to<>; -#else - using key_compare = std::equal_to; -#endif + private: + using value_pair = std::pair; + using list_type = std::list; + using list_iterator = typename list_type::iterator; + using list_const_iterator = typename list_type::const_iterator; + + // --- pointer-to-key index machinery --- + struct key_ref + { + const Key* p; + key_ref() : p(nullptr) {} + explicit key_ref(const Key& k) : p(std::addressof(k)) {} + explicit key_ref(const Key* pk) : p(pk) {} + }; + struct key_ref_hash + { + std::size_t operator()(const key_ref& kr) const + { + return std::hash {}(*kr.p); + } + }; + struct key_ref_equal + { + bool operator()(const key_ref& a, const key_ref& b) const noexcept + { + return std::equal_to {}(*a.p, *b.p); + } + }; - // Explicit constructors instead of `using Container::Container` - // otherwise older compilers choke on it (GCC <= 5.5, xcode <= 9.4) - ordered_map() noexcept(noexcept(Container())) : Container{} {} - explicit ordered_map(const Allocator& alloc) noexcept(noexcept(Container(alloc))) : Container{alloc} {} - template - ordered_map(It first, It last, const Allocator& alloc = Allocator()) - : Container{first, last, alloc} {} - ordered_map(std::initializer_list init, const Allocator& alloc = Allocator() ) - : Container{init, alloc} {} + using index_type = std::unordered_map; - std::pair emplace(const key_type& key, T&& t) + list_type m_list; + index_type m_index; + + // iterator wrapper that supports it+n for tests which do om.begin()+k + template + struct iter_base { - for (auto it = this->begin(); it != this->end(); ++it) - { - if (m_compare(it->first, key)) + using iterator_category = std::bidirectional_iterator_tag; + using value_type = value_pair; + using difference_type = typename list_type::difference_type; + using reference = Ref; + using pointer = Ptr; + + iter_base() : it_() {} + explicit iter_base(ListIter it) : it_(it) {} + + reference operator*() const + { + return *it_; + } + pointer operator->() const + { + return std::addressof(*it_); + } + + Self& operator++() + { + ++it_; + return self(); + } + Self operator++(int) + { + Self tmp = self(); + ++(*this); + return tmp; + } + Self& operator--() + { + --it_; + return self(); + } + Self operator--(int) + { + Self tmp = self(); + --(*this); + return tmp; + } + + // This is O(n), but used only by unit tests + Self operator+(difference_type n) const + { + Self tmp = self(); + if (n >= 0) // NOLINT(*-braces-around-statements) + while (n--) { - return {it, false}; + ++tmp.it_; } - } - Container::emplace_back(key, std::forward(t)); - return {std::prev(this->end()), true}; + else // NOLINT(*-braces-around-statements) + while (n++) + { + --tmp.it_; + } + return tmp; + } + Self& operator+=(difference_type n) + { + *this = *this + n; + return self(); } - template::value, int> = 0> - std::pair emplace(KeyType && key, T && t) + bool operator==(const Self& o) const + { + return it_ == o.it_; + } + bool operator!=(const Self& o) const + { + return it_ != o.it_; + } + + ListIter base() const + { + return it_; + } + + private: + ListIter it_; + Self& self() + { + return static_cast(*this); + } + const Self& self() const { - for (auto it = this->begin(); it != this->end(); ++it) + return static_cast(*this); + } + }; + + // Strong-safety rebuild: build a temporary index, then swap. + void rebuild_index() + { +#ifndef JSON_NOEXCEPTION + index_type tmp; + tmp.reserve(m_index.size()); + for (list_iterator it = m_list.begin(); it != m_list.end(); ++it) { - if (m_compare(it->first, key)) - { - return {it, false}; - } + tmp.emplace(key_ref(std::addressof(it->first)), it); + } + m_index.swap(tmp); +#else + m_index.clear(); + for (list_iterator it = m_list.begin(); it != m_list.end(); ++it) + { + m_index.emplace(key_ref(std::addressof(it->first)), it); } - Container::emplace_back(std::forward(key), std::forward(t)); - return {std::prev(this->end()), true}; +#endif } - T& operator[](const key_type& key) + // ---- C++11-safe helper: reserve index only when the iterator is at least forward ---- + template + static void reserve_index_for_range(index_type& idx, It first, It last) { - return emplace(key, T{}).first->second; + using cat_t = typename std::iterator_traits::iterator_category; + reserve_index_for_range_impl(idx, first, last, cat_t{}); } - template::value, int> = 0> - T & operator[](KeyType && key) + template + static void reserve_index_for_range_impl(index_type& idx, It first, It last, std::input_iterator_tag tag) { - return emplace(std::forward(key), T{}).first->second; + // single-pass; no pre-reserve possible } - const T& operator[](const key_type& key) const + template + static void reserve_index_for_range_impl(index_type& idx, It first, It last, std::forward_iterator_tag tag) { - return at(key); + (void)tag; + using size_type2 = typename index_type::size_type; + const auto add = static_cast(std::distance(first, last)); + idx.reserve(idx.size() + add); } - template::value, int> = 0> - const T & operator[](KeyType && key) const + // Also reserve for bidirectional iterators + template + static void reserve_index_for_range_impl(index_type& idx, It first, It last, std::bidirectional_iterator_tag tag) + { + (void)tag; + using size_type2 = typename index_type::size_type; + const auto add = static_cast(std::distance(first, last)); + idx.reserve(idx.size() + add); + } + + // And for random-access iterators + template + static void reserve_index_for_range_impl(index_type& idx, It first, It last, std::random_access_iterator_tag tag) + { + (void) tag; + using size_type2 = typename index_type::size_type; + const auto add = static_cast(std::distance(first, last)); + idx.reserve(idx.size() + add); + } + +public: + // types (match original) + using key_type = Key; + using mapped_type = T; + using value_type = value_pair; + using allocator_type = Allocator; + using size_type = typename list_type::size_type; + using difference_type = typename list_type::difference_type; + using reference = value_type&; + using const_reference = const value_type&; + using pointer = typename std::allocator_traits::pointer; + using const_pointer = typename std::allocator_traits::const_pointer; + +#if defined(JSON_HAS_CPP_14) + using key_compare = std::equal_to<>; +#else + using key_compare = std::equal_to; +#endif + + // compatibility alias (original inherited std::vector) + using Container = std::vector; + + struct iterator : iter_base + { + iterator() : iter_base() {} + explicit iterator(list_iterator it) : iter_base(it) {} + }; + + struct const_iterator : iter_base + { + const_iterator() : iter_base() {} + explicit const_iterator(list_const_iterator it) : iter_base(it) {} + const_iterator(iterator it) : iter_base(it.base()) {} + }; + + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + // constructors + ordered_map() noexcept + : m_list(), m_index() {} + + explicit ordered_map(const Allocator& alloc) noexcept + : m_list(alloc), m_index() {} + + template + ordered_map(InputIt first, InputIt last, const Allocator& alloc = Allocator()) + : m_list(alloc), m_index() + { + insert(first, last); + } + + ordered_map(std::initializer_list init, const Allocator& alloc = Allocator()) + : m_list(alloc), m_index() + { + insert(init.begin(), init.end()); + } + + ordered_map(const ordered_map& other) + : m_list(other.m_list), m_index() + { + rebuild_index(); + } + + ordered_map(ordered_map&& other) noexcept + : m_list(std::move(other.m_list)), m_index() { - return at(std::forward(key)); + rebuild_index(); } - T& at(const key_type& key) + ordered_map& operator=(const ordered_map& other) { - for (auto it = this->begin(); it != this->end(); ++it) + if (this != &other) { - if (m_compare(it->first, key)) - { - return it->second; - } + list_type tmp(other.m_list); // may throw; strong guarantee + m_list.swap(tmp); + rebuild_index(); } + return *this; + } - JSON_THROW(std::out_of_range("key not found")); + ordered_map& operator=(ordered_map&& other) noexcept + { + if (this != &other) + { + list_type tmp(std::move(other.m_list)); // noexcept move-construct + m_list.swap(tmp); + rebuild_index(); + } + return *this; } - template::value, int> = 0> - T & at(KeyType && key) // NOLINT(cppcoreguidelines-missing-std-forward) + ~ordered_map() noexcept = default; + + allocator_type get_allocator() const noexcept { - for (auto it = this->begin(); it != this->end(); ++it) + return m_list.get_allocator(); + } + + // element access + T& at(const Key& key) + { + auto it = m_index.find(key_ref(key)); + if (it == m_index.end()) { - if (m_compare(it->first, key)) - { - return it->second; - } + JSON_THROW(std::out_of_range("key not found")); } + return it->second->second; + } - JSON_THROW(std::out_of_range("key not found")); + const T& at(const Key& key) const + { + auto it = m_index.find(key_ref(key)); + if (it == m_index.end()) + { + JSON_THROW(std::out_of_range("key not found")); + } + return it->second->second; } - const T& at(const key_type& key) const + T& operator[](const Key& key) { - for (auto it = this->begin(); it != this->end(); ++it) + auto it = m_index.find(key_ref(key)); + if (it != m_index.end()) { - if (m_compare(it->first, key)) - { - return it->second; - } + return it->second->second; } + m_list.emplace_back(key, T{}); + list_iterator lit = --m_list.end(); - JSON_THROW(std::out_of_range("key not found")); +#ifndef JSON_NOEXCEPTION + JSON_TRY + { + m_index.emplace(key_ref(std::addressof(lit->first)), lit); + } + JSON_CATCH(...) + { + m_list.pop_back(); + JSON_THROW(std::bad_alloc()); + } +#else + m_index.emplace(key_ref(std::addressof(lit->first)), lit); +#endif + return lit->second; } template::value, int> = 0> - const T & at(KeyType && key) const // NOLINT(cppcoreguidelines-missing-std-forward) + T & operator[](KeyType && key) { - for (auto it = this->begin(); it != this->end(); ++it) + key_type k(std::forward(key)); + auto it = m_index.find(key_ref(k)); + if (it != m_index.end()) { - if (m_compare(it->first, key)) - { - return it->second; - } + return it->second->second; } + m_list.emplace_back(k, T{}); + list_iterator lit = --m_list.end(); - JSON_THROW(std::out_of_range("key not found")); +#ifndef JSON_NOEXCEPTION + JSON_TRY + { + m_index.emplace(key_ref(std::addressof(lit->first)), lit); + } + JSON_CATCH(...) + { + m_list.pop_back(); + JSON_THROW(std::bad_alloc()); + } +#else + m_index.emplace(key_ref(std::addressof(lit->first)), lit); +#endif + return lit->second; } - size_type erase(const key_type& key) + const T& operator[](const key_type& key) const { - for (auto it = this->begin(); it != this->end(); ++it) - { - if (m_compare(it->first, key)) - { - // Since we cannot move const Keys, re-construct them in place - for (auto next = it; ++next != this->end(); ++it) - { - it->~value_type(); // Destroy but keep allocation - new (&*it) value_type{std::move(*next)}; - } - Container::pop_back(); - return 1; - } - } - return 0; + return at(key); } template::value, int> = 0> - size_type erase(KeyType && key) // NOLINT(cppcoreguidelines-missing-std-forward) + const T & operator[](KeyType && key) const { - for (auto it = this->begin(); it != this->end(); ++it) - { - if (m_compare(it->first, key)) - { - // Since we cannot move const Keys, re-construct them in place - for (auto next = it; ++next != this->end(); ++it) - { - it->~value_type(); // Destroy but keep allocation - new (&*it) value_type{std::move(*next)}; - } - Container::pop_back(); - return 1; - } - } - return 0; + return at(key_type(std::forward(key))); } - iterator erase(iterator pos) + // iterators + iterator begin() noexcept + { + return iterator(m_list.begin()); + } + iterator end() noexcept + { + return iterator(m_list.end()); + } + const_iterator begin() const noexcept + { + return const_iterator(m_list.begin()); + } + const_iterator end() const noexcept { - return erase(pos, std::next(pos)); + return const_iterator(m_list.end()); + } + const_iterator cbegin() const noexcept + { + return const_iterator(m_list.begin()); + } + const_iterator cend() const noexcept + { + return const_iterator(m_list.end()); } - iterator erase(iterator first, iterator last) + reverse_iterator rbegin() noexcept { - if (first == last) - { - return first; - } + return reverse_iterator(end()); + } + reverse_iterator rend() noexcept + { + return reverse_iterator(begin()); + } + const_reverse_iterator rbegin() const noexcept + { + return const_reverse_iterator(end()); + } + const_reverse_iterator rend() const noexcept + { + return const_reverse_iterator(begin()); + } + const_reverse_iterator crbegin() const noexcept + { + return const_reverse_iterator(cend()); + } + const_reverse_iterator crend() const noexcept + { + return const_reverse_iterator(cbegin()); + } - const auto elements_affected = std::distance(first, last); - const auto offset = std::distance(Container::begin(), first); + // capacity + bool empty() const noexcept + { + return m_list.empty(); + } + size_type size() const noexcept + { + return m_index.size(); + } + size_type max_size() const noexcept + { + return m_list.max_size(); + } - // This is the start situation. We need to delete elements_affected - // elements (3 in this example: e, f, g), and need to return an - // iterator past the last deleted element (h in this example). - // Note that offset is the distance from the start of the vector - // to first. We will need this later. + // modifiers + void clear() noexcept + { + m_list.clear(); + m_index.clear(); + } - // [ a, b, c, d, e, f, g, h, i, j ] - // ^ ^ - // first last + std::pair insert(const value_type& value) + { + auto it = m_index.find(key_ref(value.first)); + if (it != m_index.end()) + { + return std::make_pair(iterator(it->second), false); + } + m_list.push_back(value); + list_iterator lit = --m_list.end(); - // Since we cannot move const Keys, we re-construct them in place. - // We start at first and re-construct (viz. copy) the elements from - // the back of the vector. Example for the first iteration: +#ifndef JSON_NOEXCEPTION + JSON_TRY + { + m_index.emplace(key_ref(std::addressof(lit->first)), lit); + } + JSON_CATCH(...) + { + m_list.pop_back(); + JSON_THROW(std::bad_alloc()); + } +#else + m_index.emplace(key_ref(std::addressof(lit->first)), lit); +#endif + return std::make_pair(iterator(lit), true); + } - // ,--------. - // v | destroy e and re-construct with h - // [ a, b, c, d, e, f, g, h, i, j ] - // ^ ^ - // it it + elements_affected + std::pair insert(value_type&& value) + { + auto it = m_index.find(key_ref(value.first)); + if (it != m_index.end()) + { + return std::make_pair(iterator(it->second), false); + } + m_list.push_back(std::move(value)); + list_iterator lit = --m_list.end(); - for (auto it = first; std::next(it, elements_affected) != Container::end(); ++it) +#ifndef JSON_NOEXCEPTION + JSON_TRY + { + m_index.emplace(key_ref(std::addressof(lit->first)), lit); + } + JSON_CATCH(...) { - it->~value_type(); // destroy but keep allocation - new (&*it) value_type{std::move(*std::next(it, elements_affected))}; // "move" next element to it + m_list.pop_back(); + JSON_THROW(std::bad_alloc()); } +#else + m_index.emplace(key_ref(std::addressof(lit->first)), lit); +#endif + return std::make_pair(iterator(lit), true); + } + + // hint overloads (the hint is ignored) + iterator insert(const_iterator /*hint*/, const value_type& value) + { + return insert(value).first; + } + iterator insert(const_iterator /*hint*/, value_type&& value) + { + return insert(std::move(value)).first; + } - // [ a, b, c, d, h, i, j, h, i, j ] - // ^ ^ - // first last + // keep SFINAE’d range-insert like the original + template + using require_input_iter = typename std::enable_if < + std::is_base_of::iterator_category>::value + // ReSharper disable once CppUseTypeTraitAlias + >::type; - // remove the unneeded elements at the end of the vector - Container::resize(this->size() - static_cast(elements_affected)); + template> + void insert(InputIt first, InputIt last) + { + // Optional micro-optimization: reserve only when the iterator is at least forward + reserve_index_for_range(m_index, first, last); - // [ a, b, c, d, h, i, j ] - // ^ ^ - // first last + for (; first != last; ++first) + { + insert(*first); + } + } - // first is now pointing past the last deleted element, but we cannot - // use this iterator, because it may have been invalidated by the - // resize call. Instead, we can return begin() + offset. - return Container::begin() + offset; + // vector-compat helpers (used by tests) + void push_back(const value_type& v) + { + (void)insert(v); + } + void push_back(value_type&& v) + { + (void)insert(std::move(v)); } - size_type count(const key_type& key) const + // emplace + std::pair emplace(const key_type& key, T&& t) { - for (auto it = this->begin(); it != this->end(); ++it) + auto it = m_index.find(key_ref(key)); + if (it != m_index.end()) { - if (m_compare(it->first, key)) - { - return 1; - } + return std::make_pair(iterator(it->second), false); + } + m_list.emplace_back(key, std::forward(t)); + list_iterator lit = --m_list.end(); + +#ifndef JSON_NOEXCEPTION + JSON_TRY + { + m_index.emplace(key_ref(std::addressof(lit->first)), lit); } - return 0; + JSON_CATCH(...) + { + m_list.pop_back(); + JSON_THROW(std::bad_alloc()); + } +#else + m_index.emplace(key_ref(std::addressof(lit->first)), lit); +#endif + return std::make_pair(iterator(lit), true); } template::value, int> = 0> - size_type count(KeyType && key) const // NOLINT(cppcoreguidelines-missing-std-forward) + std::pair emplace(KeyType && key, T && t) { - for (auto it = this->begin(); it != this->end(); ++it) + key_type k(std::forward(key)); + auto it = m_index.find(key_ref(k)); + if (it != m_index.end()) { - if (m_compare(it->first, key)) - { - return 1; - } + return std::make_pair(iterator(it->second), false); } - return 0; + m_list.emplace_back(k, std::forward(t)); + list_iterator lit = --m_list.end(); + +#ifndef JSON_NOEXCEPTION + JSON_TRY + { + m_index.emplace(key_ref(std::addressof(lit->first)), lit); + } + JSON_CATCH(...) + { + m_list.pop_back(); + JSON_THROW(std::bad_alloc()); + } +#else + m_index.emplace(key_ref(std::addressof(lit->first)), lit); +#endif + return std::make_pair(iterator(lit), true); + } + + // emplace_hint (hint ignored) + iterator emplace_hint(const_iterator /*hint*/, const key_type& key, T&& t) + { + return emplace(key, std::forward(t)).first; + } + template::value, int> = 0> + iterator emplace_hint(const_iterator /*hint*/, KeyType && key, T && t) + { + return emplace(std::forward(key), std::forward(t)).first; } - iterator find(const key_type& key) + // erase + size_type erase(const Key& key) { - for (auto it = this->begin(); it != this->end(); ++it) + auto it = m_index.find(key_ref(key)); + if (it == m_index.end()) { - if (m_compare(it->first, key)) - { - return it; - } + return 0; } - return Container::end(); + m_list.erase(it->second); + m_index.erase(it); + return 1; } template::value, int> = 0> - iterator find(KeyType && key) // NOLINT(cppcoreguidelines-missing-std-forward) + size_type erase(KeyType && key) { - for (auto it = this->begin(); it != this->end(); ++it) + key_type k(std::forward(key)); + auto it = m_index.find(key_ref(k)); + if (it == m_index.end()) { - if (m_compare(it->first, key)) - { - return it; - } + return 0; } - return Container::end(); + m_list.erase(it->second); + m_index.erase(it); + return 1; } - const_iterator find(const key_type& key) const + iterator erase(iterator pos) { - for (auto it = this->begin(); it != this->end(); ++it) + auto hit = m_index.find(key_ref(pos->first)); + if (hit != m_index.end()) { - if (m_compare(it->first, key)) - { - return it; - } + m_index.erase(hit); } - return Container::end(); + list_iterator next = m_list.erase(pos.base()); + return iterator(next); } - std::pair insert( value_type&& value ) + iterator erase(const_iterator pos) { - return emplace(value.first, std::move(value.second)); + auto hit = m_index.find(key_ref(pos->first)); + if (hit != m_index.end()) + { + m_index.erase(hit); + } + list_iterator next = m_list.erase(pos.base()); + return iterator(next); } - std::pair insert( const value_type& value ) + iterator erase(iterator first, iterator last) { - for (auto it = this->begin(); it != this->end(); ++it) + for (iterator it = first; it != last; ++it) { - if (m_compare(it->first, value.first)) + auto hit = m_index.find(key_ref(it->first)); + if (hit != m_index.end()) { - return {it, false}; + m_index.erase(hit); } } - Container::push_back(value); - return {--this->end(), true}; + list_iterator ret = m_list.erase(first.base(), last.base()); + return iterator(ret); } - template - using require_input_iter = typename std::enable_if::iterator_category, - std::input_iterator_tag>::value>::type; - - template> - void insert(InputIt first, InputIt last) + iterator erase(const_iterator first, const_iterator last) { - for (auto it = first; it != last; ++it) + for (const_iterator it = first; it != last; ++it) { - insert(*it); + auto hit = m_index.find(key_ref(it->first)); + if (hit != m_index.end()) + { + m_index.erase(hit); + } } + list_iterator ret = m_list.erase(first.base(), last.base()); + return iterator(ret); } -private: - JSON_NO_UNIQUE_ADDRESS key_compare m_compare = key_compare(); + // lookup + size_type count(const Key& key) const + { + return static_cast(m_index.count(key_ref(key))); + } + + template::value, int> = 0> + size_type count(KeyType && key) const + { + key_type k(std::forward(key)); + return static_cast(m_index.count(key_ref(k))); + } + + iterator find(const Key& key) + { + auto it = m_index.find(key_ref(key)); + return (it == m_index.end()) ? iterator(m_list.end()) : iterator(it->second); + } + + const_iterator find(const Key& key) const + { + auto it = m_index.find(key_ref(key)); + return (it == m_index.end()) ? const_iterator(m_list.end()) : const_iterator(it->second); + } + + template::value, int> = 0> + iterator find(KeyType && key) + { + key_type k(std::forward(key)); + auto it = m_index.find(key_ref(k)); + return (it == m_index.end()) ? iterator(m_list.end()) : iterator(it->second); + } + + template::value, int> = 0> + const_iterator find(KeyType && key) const + { + key_type k(std::forward(key)); + auto it = m_index.find(key_ref(k)); + return (it == m_index.end()) ? const_iterator(m_list.end()) : const_iterator(it->second); + } + + // comparison (preserve vector-like sequence comparison) + friend bool operator==(const ordered_map& lhs, const ordered_map& rhs) + { + return lhs.m_list == rhs.m_list; + } + friend bool operator!=(const ordered_map& lhs, const ordered_map& rhs) + { + return !(lhs == rhs); + } + friend bool operator<(const ordered_map& lhs, const ordered_map& rhs) + { + return std::lexicographical_compare(lhs.m_list.begin(), lhs.m_list.end(), + rhs.m_list.begin(), rhs.m_list.end()); + } + friend bool operator>(const ordered_map& lhs, const ordered_map& rhs) + { + return rhs < lhs; + } + friend bool operator<=(const ordered_map& lhs, const ordered_map& rhs) + { + return !(rhs < lhs); + } + friend bool operator>=(const ordered_map& lhs, const ordered_map& rhs) + { + return !(lhs < rhs); + } + + // swap + void swap(ordered_map& other) noexcept + { + m_list.swap(other.m_list); + m_index.swap(other.m_index); + } + friend void swap(ordered_map& a, ordered_map& b) noexcept + { + a.swap(b); + } }; NLOHMANN_JSON_NAMESPACE_END diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index aafeaf5ae5..7ebd0ca7d3 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -19812,14 +19812,18 @@ NLOHMANN_JSON_NAMESPACE_END -#include // equal_to, less -#include // initializer_list -#include // input_iterator_tag, iterator_traits -#include // allocator -#include // for out_of_range -#include // enable_if, is_convertible -#include // pair -#include // vector +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // #include @@ -19827,339 +19831,803 @@ NLOHMANN_JSON_NAMESPACE_END NLOHMANN_JSON_NAMESPACE_BEGIN +/*! +@brief Insertion-order-preserving map with O(1) average lookup -/// ordered_map: a minimal map-like container that preserves insertion order -/// for use within nlohmann::basic_json -template , - class Allocator = std::allocator>> - struct ordered_map : std::vector, Allocator> -{ - using key_type = Key; - using mapped_type = T; - using Container = std::vector, Allocator>; - using iterator = typename Container::iterator; - using const_iterator = typename Container::const_iterator; - using size_type = typename Container::size_type; - using value_type = typename Container::value_type; -#ifdef JSON_HAS_CPP_14 - using key_compare = std::equal_to<>; -#else - using key_compare = std::equal_to; -#endif +Internally uses a std::list to contain the pairs, plus +an index (effectively keyed by a pointer-to-key) for O(1) lookups. - // Explicit constructors instead of `using Container::Container` - // otherwise older compilers choke on it (GCC <= 5.5, xcode <= 9.4) - ordered_map() noexcept(noexcept(Container())) : Container{} {} - explicit ordered_map(const Allocator& alloc) noexcept(noexcept(Container(alloc))) : Container{alloc} {} - template - ordered_map(It first, It last, const Allocator& alloc = Allocator()) - : Container{first, last, alloc} {} - ordered_map(std::initializer_list init, const Allocator& alloc = Allocator() ) - : Container{init, alloc} {} +This implements all the methods of std::map except: +- Ordering-dependent operations (lower_bound, upper_bound, equal_range, + key_comp and value_comp, etc.); these simply make no sense without + lexicographic ordering. +- Extensions added after C++11 (try_emplace, insert_or_assign, contains, + erase_if, etc.). - std::pair emplace(const key_type& key, T&& t) +It also has iterator arithmetic (it+n), which is not used by the library +but needed by some unit tests of the original implementation. + +Stable iterators guarantee: insertion and value updates do not invalidate +iterators (except for the iterator to an element that is erased). + +Size complexity is linear to the size of contained data; time complexity +is on average O(1) for all access methods and all methods that change a +single element, O(N) when N elements are affected. +*/ +template, // Unused + class Allocator = std::allocator>> + struct ordered_map +{ + private: + using value_pair = std::pair; + using list_type = std::list; + using list_iterator = typename list_type::iterator; + using list_const_iterator = typename list_type::const_iterator; + + // --- pointer-to-key index machinery --- + struct key_ref + { + const Key* p; + key_ref() : p(nullptr) {} + explicit key_ref(const Key& k) : p(std::addressof(k)) {} + explicit key_ref(const Key* pk) : p(pk) {} + }; + struct key_ref_hash { - for (auto it = this->begin(); it != this->end(); ++it) + std::size_t operator()(const key_ref& kr) const { - if (m_compare(it->first, key)) - { - return {it, false}; - } + return std::hash {}(*kr.p); } - Container::emplace_back(key, std::forward(t)); - return {std::prev(this->end()), true}; + }; + struct key_ref_equal + { + bool operator()(const key_ref& a, const key_ref& b) const noexcept + { + return std::equal_to {}(*a.p, *b.p); + } + }; + + using index_type = std::unordered_map; + + list_type m_list; + index_type m_index; + + // iterator wrapper that supports it+n for tests which do om.begin()+k + template + struct iter_base + { + using iterator_category = std::bidirectional_iterator_tag; + using value_type = value_pair; + using difference_type = typename list_type::difference_type; + using reference = Ref; + using pointer = Ptr; + + iter_base() : it_() {} + explicit iter_base(ListIter it) : it_(it) {} + + reference operator*() const + { + return *it_; + } + pointer operator->() const + { + return std::addressof(*it_); } - template::value, int> = 0> - std::pair emplace(KeyType && key, T && t) + Self& operator++() { - for (auto it = this->begin(); it != this->end(); ++it) - { - if (m_compare(it->first, key)) + ++it_; + return self(); + } + Self operator++(int) + { + Self tmp = self(); + ++(*this); + return tmp; + } + Self& operator--() + { + --it_; + return self(); + } + Self operator--(int) + { + Self tmp = self(); + --(*this); + return tmp; + } + + // This is O(n), but used only by unit tests + Self operator+(difference_type n) const + { + Self tmp = self(); + if (n >= 0) // NOLINT(*-braces-around-statements) + while (n--) { - return {it, false}; + ++tmp.it_; } + else // NOLINT(*-braces-around-statements) + while (n++) + { + --tmp.it_; + } + return tmp; + } + Self& operator+=(difference_type n) + { + *this = *this + n; + return self(); + } + + bool operator==(const Self& o) const + { + return it_ == o.it_; + } + bool operator!=(const Self& o) const + { + return it_ != o.it_; + } + + ListIter base() const + { + return it_; + } + + private: + ListIter it_; + Self& self() + { + return static_cast(*this); + } + const Self& self() const + { + return static_cast(*this); + } + }; + + // Strong-safety rebuild: build a temporary index, then swap. + void rebuild_index() + { +#ifndef JSON_NOEXCEPTION + index_type tmp; + tmp.reserve(m_index.size()); + for (list_iterator it = m_list.begin(); it != m_list.end(); ++it) + { + tmp.emplace(key_ref(std::addressof(it->first)), it); } - Container::emplace_back(std::forward(key), std::forward(t)); - return {std::prev(this->end()), true}; + m_index.swap(tmp); +#else + m_index.clear(); + for (list_iterator it = m_list.begin(); it != m_list.end(); ++it) + { + m_index.emplace(key_ref(std::addressof(it->first)), it); + } +#endif } - T& operator[](const key_type& key) + // ---- C++11-safe helper: reserve index only when the iterator is at least forward ---- + template + static void reserve_index_for_range(index_type& idx, It first, It last) { - return emplace(key, T{}).first->second; + using cat_t = typename std::iterator_traits::iterator_category; + reserve_index_for_range_impl(idx, first, last, cat_t{}); } - template::value, int> = 0> - T & operator[](KeyType && key) + template + static void reserve_index_for_range_impl(index_type& idx, It first, It last, std::input_iterator_tag tag) { - return emplace(std::forward(key), T{}).first->second; + // single-pass; no pre-reserve possible } - const T& operator[](const key_type& key) const + template + static void reserve_index_for_range_impl(index_type& idx, It first, It last, std::forward_iterator_tag tag) { - return at(key); + (void)tag; + using size_type2 = typename index_type::size_type; + const auto add = static_cast(std::distance(first, last)); + idx.reserve(idx.size() + add); } - template::value, int> = 0> - const T & operator[](KeyType && key) const + // Also reserve for bidirectional iterators + template + static void reserve_index_for_range_impl(index_type& idx, It first, It last, std::bidirectional_iterator_tag tag) { - return at(std::forward(key)); + (void)tag; + using size_type2 = typename index_type::size_type; + const auto add = static_cast(std::distance(first, last)); + idx.reserve(idx.size() + add); } - T& at(const key_type& key) + // And for random-access iterators + template + static void reserve_index_for_range_impl(index_type& idx, It first, It last, std::random_access_iterator_tag tag) { - for (auto it = this->begin(); it != this->end(); ++it) + (void) tag; + using size_type2 = typename index_type::size_type; + const auto add = static_cast(std::distance(first, last)); + idx.reserve(idx.size() + add); + } + +public: + // types (match original) + using key_type = Key; + using mapped_type = T; + using value_type = value_pair; + using allocator_type = Allocator; + using size_type = typename list_type::size_type; + using difference_type = typename list_type::difference_type; + using reference = value_type&; + using const_reference = const value_type&; + using pointer = typename std::allocator_traits::pointer; + using const_pointer = typename std::allocator_traits::const_pointer; + +#if defined(JSON_HAS_CPP_14) + using key_compare = std::equal_to<>; +#else + using key_compare = std::equal_to; +#endif + + // compatibility alias (original inherited std::vector) + using Container = std::vector; + + struct iterator : iter_base + { + iterator() : iter_base() {} + explicit iterator(list_iterator it) : iter_base(it) {} + }; + + struct const_iterator : iter_base + { + const_iterator() : iter_base() {} + explicit const_iterator(list_const_iterator it) : iter_base(it) {} + const_iterator(iterator it) : iter_base(it.base()) {} + }; + + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + // constructors + ordered_map() noexcept + : m_list(), m_index() {} + + explicit ordered_map(const Allocator& alloc) noexcept + : m_list(alloc), m_index() {} + + template + ordered_map(InputIt first, InputIt last, const Allocator& alloc = Allocator()) + : m_list(alloc), m_index() + { + insert(first, last); + } + + ordered_map(std::initializer_list init, const Allocator& alloc = Allocator()) + : m_list(alloc), m_index() + { + insert(init.begin(), init.end()); + } + + ordered_map(const ordered_map& other) + : m_list(other.m_list), m_index() + { + rebuild_index(); + } + + ordered_map(ordered_map&& other) noexcept + : m_list(std::move(other.m_list)), m_index() + { + rebuild_index(); + } + + ordered_map& operator=(const ordered_map& other) + { + if (this != &other) { - if (m_compare(it->first, key)) - { - return it->second; - } + list_type tmp(other.m_list); // may throw; strong guarantee + m_list.swap(tmp); + rebuild_index(); + } + return *this; + } + + ordered_map& operator=(ordered_map&& other) noexcept + { + if (this != &other) + { + list_type tmp(std::move(other.m_list)); // noexcept move-construct + m_list.swap(tmp); + rebuild_index(); } + return *this; + } + + ~ordered_map() noexcept = default; - JSON_THROW(std::out_of_range("key not found")); + allocator_type get_allocator() const noexcept + { + return m_list.get_allocator(); } - template::value, int> = 0> - T & at(KeyType && key) // NOLINT(cppcoreguidelines-missing-std-forward) + // element access + T& at(const Key& key) { - for (auto it = this->begin(); it != this->end(); ++it) + auto it = m_index.find(key_ref(key)); + if (it == m_index.end()) { - if (m_compare(it->first, key)) - { - return it->second; - } + JSON_THROW(std::out_of_range("key not found")); } + return it->second->second; + } - JSON_THROW(std::out_of_range("key not found")); + const T& at(const Key& key) const + { + auto it = m_index.find(key_ref(key)); + if (it == m_index.end()) + { + JSON_THROW(std::out_of_range("key not found")); + } + return it->second->second; } - const T& at(const key_type& key) const + T& operator[](const Key& key) { - for (auto it = this->begin(); it != this->end(); ++it) + auto it = m_index.find(key_ref(key)); + if (it != m_index.end()) { - if (m_compare(it->first, key)) - { - return it->second; - } + return it->second->second; } + m_list.emplace_back(key, T{}); + list_iterator lit = --m_list.end(); - JSON_THROW(std::out_of_range("key not found")); +#ifndef JSON_NOEXCEPTION + JSON_TRY + { + m_index.emplace(key_ref(std::addressof(lit->first)), lit); + } + JSON_CATCH(...) + { + m_list.pop_back(); + JSON_THROW(std::bad_alloc()); + } +#else + m_index.emplace(key_ref(std::addressof(lit->first)), lit); +#endif + return lit->second; } template::value, int> = 0> - const T & at(KeyType && key) const // NOLINT(cppcoreguidelines-missing-std-forward) + T & operator[](KeyType && key) { - for (auto it = this->begin(); it != this->end(); ++it) + key_type k(std::forward(key)); + auto it = m_index.find(key_ref(k)); + if (it != m_index.end()) { - if (m_compare(it->first, key)) - { - return it->second; - } + return it->second->second; } + m_list.emplace_back(k, T{}); + list_iterator lit = --m_list.end(); - JSON_THROW(std::out_of_range("key not found")); +#ifndef JSON_NOEXCEPTION + JSON_TRY + { + m_index.emplace(key_ref(std::addressof(lit->first)), lit); + } + JSON_CATCH(...) + { + m_list.pop_back(); + JSON_THROW(std::bad_alloc()); + } +#else + m_index.emplace(key_ref(std::addressof(lit->first)), lit); +#endif + return lit->second; } - size_type erase(const key_type& key) + const T& operator[](const key_type& key) const { - for (auto it = this->begin(); it != this->end(); ++it) - { - if (m_compare(it->first, key)) - { - // Since we cannot move const Keys, re-construct them in place - for (auto next = it; ++next != this->end(); ++it) - { - it->~value_type(); // Destroy but keep allocation - new (&*it) value_type{std::move(*next)}; - } - Container::pop_back(); - return 1; - } - } - return 0; + return at(key); } template::value, int> = 0> - size_type erase(KeyType && key) // NOLINT(cppcoreguidelines-missing-std-forward) + const T & operator[](KeyType && key) const { - for (auto it = this->begin(); it != this->end(); ++it) - { - if (m_compare(it->first, key)) - { - // Since we cannot move const Keys, re-construct them in place - for (auto next = it; ++next != this->end(); ++it) - { - it->~value_type(); // Destroy but keep allocation - new (&*it) value_type{std::move(*next)}; - } - Container::pop_back(); - return 1; - } - } - return 0; + return at(key_type(std::forward(key))); } - iterator erase(iterator pos) + // iterators + iterator begin() noexcept { - return erase(pos, std::next(pos)); + return iterator(m_list.begin()); + } + iterator end() noexcept + { + return iterator(m_list.end()); + } + const_iterator begin() const noexcept + { + return const_iterator(m_list.begin()); + } + const_iterator end() const noexcept + { + return const_iterator(m_list.end()); + } + const_iterator cbegin() const noexcept + { + return const_iterator(m_list.begin()); + } + const_iterator cend() const noexcept + { + return const_iterator(m_list.end()); } - iterator erase(iterator first, iterator last) + reverse_iterator rbegin() noexcept { - if (first == last) - { - return first; - } + return reverse_iterator(end()); + } + reverse_iterator rend() noexcept + { + return reverse_iterator(begin()); + } + const_reverse_iterator rbegin() const noexcept + { + return const_reverse_iterator(end()); + } + const_reverse_iterator rend() const noexcept + { + return const_reverse_iterator(begin()); + } + const_reverse_iterator crbegin() const noexcept + { + return const_reverse_iterator(cend()); + } + const_reverse_iterator crend() const noexcept + { + return const_reverse_iterator(cbegin()); + } - const auto elements_affected = std::distance(first, last); - const auto offset = std::distance(Container::begin(), first); + // capacity + bool empty() const noexcept + { + return m_list.empty(); + } + size_type size() const noexcept + { + return m_index.size(); + } + size_type max_size() const noexcept + { + return m_list.max_size(); + } - // This is the start situation. We need to delete elements_affected - // elements (3 in this example: e, f, g), and need to return an - // iterator past the last deleted element (h in this example). - // Note that offset is the distance from the start of the vector - // to first. We will need this later. + // modifiers + void clear() noexcept + { + m_list.clear(); + m_index.clear(); + } - // [ a, b, c, d, e, f, g, h, i, j ] - // ^ ^ - // first last + std::pair insert(const value_type& value) + { + auto it = m_index.find(key_ref(value.first)); + if (it != m_index.end()) + { + return std::make_pair(iterator(it->second), false); + } + m_list.push_back(value); + list_iterator lit = --m_list.end(); - // Since we cannot move const Keys, we re-construct them in place. - // We start at first and re-construct (viz. copy) the elements from - // the back of the vector. Example for the first iteration: +#ifndef JSON_NOEXCEPTION + JSON_TRY + { + m_index.emplace(key_ref(std::addressof(lit->first)), lit); + } + JSON_CATCH(...) + { + m_list.pop_back(); + JSON_THROW(std::bad_alloc()); + } +#else + m_index.emplace(key_ref(std::addressof(lit->first)), lit); +#endif + return std::make_pair(iterator(lit), true); + } - // ,--------. - // v | destroy e and re-construct with h - // [ a, b, c, d, e, f, g, h, i, j ] - // ^ ^ - // it it + elements_affected + std::pair insert(value_type&& value) + { + auto it = m_index.find(key_ref(value.first)); + if (it != m_index.end()) + { + return std::make_pair(iterator(it->second), false); + } + m_list.push_back(std::move(value)); + list_iterator lit = --m_list.end(); - for (auto it = first; std::next(it, elements_affected) != Container::end(); ++it) +#ifndef JSON_NOEXCEPTION + JSON_TRY + { + m_index.emplace(key_ref(std::addressof(lit->first)), lit); + } + JSON_CATCH(...) { - it->~value_type(); // destroy but keep allocation - new (&*it) value_type{std::move(*std::next(it, elements_affected))}; // "move" next element to it + m_list.pop_back(); + JSON_THROW(std::bad_alloc()); } +#else + m_index.emplace(key_ref(std::addressof(lit->first)), lit); +#endif + return std::make_pair(iterator(lit), true); + } + + // hint overloads (the hint is ignored) + iterator insert(const_iterator /*hint*/, const value_type& value) + { + return insert(value).first; + } + iterator insert(const_iterator /*hint*/, value_type&& value) + { + return insert(std::move(value)).first; + } - // [ a, b, c, d, h, i, j, h, i, j ] - // ^ ^ - // first last + // keep SFINAE’d range-insert like the original + template + using require_input_iter = typename std::enable_if < + std::is_base_of::iterator_category>::value + // ReSharper disable once CppUseTypeTraitAlias + >::type; - // remove the unneeded elements at the end of the vector - Container::resize(this->size() - static_cast(elements_affected)); + template> + void insert(InputIt first, InputIt last) + { + // Optional micro-optimization: reserve only when the iterator is at least forward + reserve_index_for_range(m_index, first, last); - // [ a, b, c, d, h, i, j ] - // ^ ^ - // first last + for (; first != last; ++first) + { + insert(*first); + } + } - // first is now pointing past the last deleted element, but we cannot - // use this iterator, because it may have been invalidated by the - // resize call. Instead, we can return begin() + offset. - return Container::begin() + offset; + // vector-compat helpers (used by tests) + void push_back(const value_type& v) + { + (void)insert(v); + } + void push_back(value_type&& v) + { + (void)insert(std::move(v)); } - size_type count(const key_type& key) const + // emplace + std::pair emplace(const key_type& key, T&& t) { - for (auto it = this->begin(); it != this->end(); ++it) + auto it = m_index.find(key_ref(key)); + if (it != m_index.end()) { - if (m_compare(it->first, key)) - { - return 1; - } + return std::make_pair(iterator(it->second), false); } - return 0; + m_list.emplace_back(key, std::forward(t)); + list_iterator lit = --m_list.end(); + +#ifndef JSON_NOEXCEPTION + JSON_TRY + { + m_index.emplace(key_ref(std::addressof(lit->first)), lit); + } + JSON_CATCH(...) + { + m_list.pop_back(); + JSON_THROW(std::bad_alloc()); + } +#else + m_index.emplace(key_ref(std::addressof(lit->first)), lit); +#endif + return std::make_pair(iterator(lit), true); } template::value, int> = 0> - size_type count(KeyType && key) const // NOLINT(cppcoreguidelines-missing-std-forward) + std::pair emplace(KeyType && key, T && t) { - for (auto it = this->begin(); it != this->end(); ++it) + key_type k(std::forward(key)); + auto it = m_index.find(key_ref(k)); + if (it != m_index.end()) { - if (m_compare(it->first, key)) - { - return 1; - } + return std::make_pair(iterator(it->second), false); } - return 0; + m_list.emplace_back(k, std::forward(t)); + list_iterator lit = --m_list.end(); + +#ifndef JSON_NOEXCEPTION + JSON_TRY + { + m_index.emplace(key_ref(std::addressof(lit->first)), lit); + } + JSON_CATCH(...) + { + m_list.pop_back(); + JSON_THROW(std::bad_alloc()); + } +#else + m_index.emplace(key_ref(std::addressof(lit->first)), lit); +#endif + return std::make_pair(iterator(lit), true); + } + + // emplace_hint (hint ignored) + iterator emplace_hint(const_iterator /*hint*/, const key_type& key, T&& t) + { + return emplace(key, std::forward(t)).first; + } + template::value, int> = 0> + iterator emplace_hint(const_iterator /*hint*/, KeyType && key, T && t) + { + return emplace(std::forward(key), std::forward(t)).first; } - iterator find(const key_type& key) + // erase + size_type erase(const Key& key) { - for (auto it = this->begin(); it != this->end(); ++it) + auto it = m_index.find(key_ref(key)); + if (it == m_index.end()) { - if (m_compare(it->first, key)) - { - return it; - } + return 0; } - return Container::end(); + m_list.erase(it->second); + m_index.erase(it); + return 1; } template::value, int> = 0> - iterator find(KeyType && key) // NOLINT(cppcoreguidelines-missing-std-forward) + size_type erase(KeyType && key) { - for (auto it = this->begin(); it != this->end(); ++it) + key_type k(std::forward(key)); + auto it = m_index.find(key_ref(k)); + if (it == m_index.end()) { - if (m_compare(it->first, key)) - { - return it; - } + return 0; } - return Container::end(); + m_list.erase(it->second); + m_index.erase(it); + return 1; } - const_iterator find(const key_type& key) const + iterator erase(iterator pos) { - for (auto it = this->begin(); it != this->end(); ++it) + auto hit = m_index.find(key_ref(pos->first)); + if (hit != m_index.end()) { - if (m_compare(it->first, key)) - { - return it; - } + m_index.erase(hit); } - return Container::end(); + list_iterator next = m_list.erase(pos.base()); + return iterator(next); } - std::pair insert( value_type&& value ) + iterator erase(const_iterator pos) { - return emplace(value.first, std::move(value.second)); + auto hit = m_index.find(key_ref(pos->first)); + if (hit != m_index.end()) + { + m_index.erase(hit); + } + list_iterator next = m_list.erase(pos.base()); + return iterator(next); } - std::pair insert( const value_type& value ) + iterator erase(iterator first, iterator last) { - for (auto it = this->begin(); it != this->end(); ++it) + for (iterator it = first; it != last; ++it) { - if (m_compare(it->first, value.first)) + auto hit = m_index.find(key_ref(it->first)); + if (hit != m_index.end()) { - return {it, false}; + m_index.erase(hit); } } - Container::push_back(value); - return {--this->end(), true}; + list_iterator ret = m_list.erase(first.base(), last.base()); + return iterator(ret); } - template - using require_input_iter = typename std::enable_if::iterator_category, - std::input_iterator_tag>::value>::type; - - template> - void insert(InputIt first, InputIt last) + iterator erase(const_iterator first, const_iterator last) { - for (auto it = first; it != last; ++it) + for (const_iterator it = first; it != last; ++it) { - insert(*it); + auto hit = m_index.find(key_ref(it->first)); + if (hit != m_index.end()) + { + m_index.erase(hit); + } } + list_iterator ret = m_list.erase(first.base(), last.base()); + return iterator(ret); } -private: - JSON_NO_UNIQUE_ADDRESS key_compare m_compare = key_compare(); + // lookup + size_type count(const Key& key) const + { + return static_cast(m_index.count(key_ref(key))); + } + + template::value, int> = 0> + size_type count(KeyType && key) const + { + key_type k(std::forward(key)); + return static_cast(m_index.count(key_ref(k))); + } + + iterator find(const Key& key) + { + auto it = m_index.find(key_ref(key)); + return (it == m_index.end()) ? iterator(m_list.end()) : iterator(it->second); + } + + const_iterator find(const Key& key) const + { + auto it = m_index.find(key_ref(key)); + return (it == m_index.end()) ? const_iterator(m_list.end()) : const_iterator(it->second); + } + + template::value, int> = 0> + iterator find(KeyType && key) + { + key_type k(std::forward(key)); + auto it = m_index.find(key_ref(k)); + return (it == m_index.end()) ? iterator(m_list.end()) : iterator(it->second); + } + + template::value, int> = 0> + const_iterator find(KeyType && key) const + { + key_type k(std::forward(key)); + auto it = m_index.find(key_ref(k)); + return (it == m_index.end()) ? const_iterator(m_list.end()) : const_iterator(it->second); + } + + // comparison (preserve vector-like sequence comparison) + friend bool operator==(const ordered_map& lhs, const ordered_map& rhs) + { + return lhs.m_list == rhs.m_list; + } + friend bool operator!=(const ordered_map& lhs, const ordered_map& rhs) + { + return !(lhs == rhs); + } + friend bool operator<(const ordered_map& lhs, const ordered_map& rhs) + { + return std::lexicographical_compare(lhs.m_list.begin(), lhs.m_list.end(), + rhs.m_list.begin(), rhs.m_list.end()); + } + friend bool operator>(const ordered_map& lhs, const ordered_map& rhs) + { + return rhs < lhs; + } + friend bool operator<=(const ordered_map& lhs, const ordered_map& rhs) + { + return !(rhs < lhs); + } + friend bool operator>=(const ordered_map& lhs, const ordered_map& rhs) + { + return !(lhs < rhs); + } + + // swap + void swap(ordered_map& other) noexcept + { + m_list.swap(other.m_list); + m_index.swap(other.m_index); + } + friend void swap(ordered_map& a, ordered_map& b) noexcept + { + a.swap(b); + } }; NLOHMANN_JSON_NAMESPACE_END diff --git a/tests/src/unit-ordered-json-performance.cpp b/tests/src/unit-ordered-json-performance.cpp new file mode 100644 index 0000000000..224af9ec97 --- /dev/null +++ b/tests/src/unit-ordered-json-performance.cpp @@ -0,0 +1,245 @@ +// +// Created by Andrea Cocito on 05/10/25. +// +#include "doctest.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__APPLE__) + #include + #include +#elif defined(__unix__) || defined(__linux__) + #include + #include +#endif + +using nlohmann::ordered_json; + +namespace +{ + +// ---- timing & memory helpers ------------------------------------------------ + +using clock_tp = std::chrono::steady_clock::time_point; + +inline clock_tp now() +{ + return std::chrono::steady_clock::now(); +} + +struct CpuTimes +{ + long long user_us = 0; + long long sys_us = 0; +}; + +inline CpuTimes cpu_now() +{ + CpuTimes t{}; +#if defined(__APPLE__) || defined(__unix__) || defined(__linux__) + rusage ru {}; + getrusage(RUSAGE_SELF, &ru); + t.user_us = static_cast(ru.ru_utime.tv_sec) * 1000000LL + ru.ru_utime.tv_usec; + t.sys_us = static_cast(ru.ru_stime.tv_sec) * 1000000LL + ru.ru_stime.tv_usec; +#endif + return t; +} + +inline std::size_t current_rss_bytes() +{ +#if defined(__APPLE__) + mach_task_basic_info info {}; + mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT; + if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, reinterpret_cast(&info), &count) == KERN_SUCCESS) + { + return static_cast(info.resident_size); + } + return 0; +#elif defined(__linux__) + // ru_maxrss is in kilobytes on Linux — it's *peak* RSS, not current. + rusage ru{}; + getrusage(RUSAGE_SELF, &ru); + return static_cast(ru.ru_maxrss) * 1024ULL; +#elif defined(__unix__) + // Fallback POSIX: ru_maxrss unit is implementation-defined. Treat as bytes if huge, else KB. + rusage ru{}; + getrusage(RUSAGE_SELF, &ru); + std::size_t v = static_cast(ru.ru_maxrss); + if (v < (1ULL << 20)) + { + v *= 1024ULL; // heuristic: likely KB + } + return v; +#else + return 0; // unsupported platform +#endif +} + +inline std::string fmt_bytes(std::size_t b) +{ + const char* units[] = {"B", "KiB", "MiB", "GiB", "TiB"}; + double val = static_cast(b); + int u = 0; + while (val >= 1024.0 && u < 4) + { + val /= 1024.0; + ++u; + } + std::ostringstream oss; + oss << std::fixed << std::setprecision(2) << val << ' ' << units[u]; + return oss.str(); +} + +struct PhaseTimer +{ + clock_tp w0 = now(); + CpuTimes c0 = cpu_now(); + std::size_t rss0 = current_rss_bytes(); + + void print(const char* label) + { + auto w1 = now(); + CpuTimes c1 = cpu_now(); + std::size_t rss1 = current_rss_bytes(); + + const double wall_ms = std::chrono::duration(w1 - w0).count(); + const double cpu_ms = static_cast((c1.user_us - c0.user_us) + (c1.sys_us - c0.sys_us)) / 1000.0; + + std::cout << std::left << std::setw(28) << label + << " | wall: " << std::setw(10) << std::fixed << std::setprecision(3) << wall_ms << " ms" + << " | cpu: " << std::setw(10) << std::fixed << std::setprecision(3) << cpu_ms << " ms" + << " | RSS: " << fmt_bytes(rss1) +#if !defined(__APPLE__) + << " (peak on this platform)" +#endif + << '\n'; + // reset for next phase + w0 = w1; + c0 = c1; + rss0 = rss1; + } +}; + +// ---- random data helpers ---------------------------------------------------- + +inline std::string random_hex32(std::mt19937_64& rng) +{ + static const char* hex = "0123456789abcdef"; + // 16 random bytes -> 32 hex chars + char out[32]; + for (int i = 0; i < 4; ++i) + { + uint64_t x = rng(); + // take 16 hex chars from 64 bits (we need 32 total -> 4*8 nibbles) + for (int n = 0; n < 8; ++n) + { + out[i * 8 + (7 - n)] = hex[(x & 0xFULL)]; + x >>= 4; + } + } + return std::string(out, out + 32); +} + +inline std::vector make_keys(std::size_t N, std::mt19937_64& rng) +{ + std::vector v; + v.reserve(N); + for (std::size_t i = 0; i < N; ++i) + { + v.push_back(random_hex32(rng)); + } + return v; +} + +inline std::vector shuffled_indices(std::size_t N, std::mt19937_64& rng) +{ + std::vector idx(N); + for (std::size_t i = 0; i < N; ++i) + { + idx[i] = i; + } + std::shuffle(idx.begin(), idx.end(), rng); + return idx; +} + +} // namespace + +TEST_CASE("ordered_map perf: fill, access, erase") +{ + // N: default 20000; override with env NLOHMANN_OM_BENCH_N + std::size_t N = 20000; + if (const char* env = std::getenv("NLOHMANN_OM_BENCH_N")) + { + try + { + N = static_cast(std::stoull(env)); + } + catch (...) { /* keep default */ } + } + // RNG seed: fixed by default for repeatability; override with env NLOHMANN_OM_BENCH_SEED + uint64_t seed = 0xC0FFEE123456789ULL; + if (const char* s = std::getenv("NLOHMANN_OM_BENCH_SEED")) + { + try + { + seed = std::stoull(s); + } + catch (...) {} + } + std::mt19937_64 rng(seed); + + std::cout << "[ordered_map perf] N=" << N << " seed=" << seed << "\n"; + + PhaseTimer T; + + // (a) make base keys and 3 random orders + auto keys = make_keys(N, rng); + auto order1 = shuffled_indices(N, rng); + auto order2 = shuffled_indices(N, rng); + auto order3 = shuffled_indices(N, rng); + T.print("a) generated keys & shuffles"); + + // (b) fill ordered_json j with j[keys[order1[i]]] = i + ordered_json j = ordered_json::object(); + for (std::size_t i = 0; i < N; ++i) + { + j[keys[order1[i]]] = static_cast(i); + } + // sanity + CHECK(j.size() == N); + T.print("b) filled object"); + + // (c) access in random order and sum results + volatile std::uint64_t sum = 0; + for (std::size_t i = 0; i < N; ++i) + { + const auto& k = keys[order2[i]]; + sum += static_cast(j[k].get()); + } + // keep compiler from optimizing away + std::cout << " sum=" << sum << "\n"; + T.print("c) random access"); + + // (d) erase in random order + std::size_t removed = 0; + for (std::size_t i = 0; i < N; ++i) + { + const auto& k = keys[order3[i]]; + removed += j.erase(k); + } + CHECK(removed == N); + CHECK(j.empty()); + T.print("d) random erase"); +} diff --git a/tests/src/unit-ordered-map-2.cpp b/tests/src/unit-ordered-map-2.cpp new file mode 100644 index 0000000000..a7319d8df8 --- /dev/null +++ b/tests/src/unit-ordered-map-2.cpp @@ -0,0 +1,310 @@ +// tests/src/unit-ordered-map-2.cpp +// +// Extra tests for nlohmann::ordered_map – aims to exercise all public API +// paths we rely on (C++11), including iterator arithmetic and exception +// safety. This file intentionally does NOT define a doctest main +// (the project already provides one). + +#include "doctest.h" + +#include + +#include +#include +#include +#include + +// ---------- Throwing key to exercise strong-exception-safety paths ---------- +struct ThrowingKey +{ + std::string s; + static bool inject; // toggled by tests + + ThrowingKey() = default; + /* implicit */ ThrowingKey(const char* cs) : s(cs) {} + explicit ThrowingKey(std::string v) : s(std::move(v)) {} + + bool operator==(const ThrowingKey& o) const noexcept + { + return s == o.s; + } + bool operator!=(const ThrowingKey& o) const noexcept + { + return !(*this == o); + } +}; + +bool ThrowingKey::inject = false; + +namespace std +{ +template<> struct hash +{ + size_t operator()(const ThrowingKey& k) const + { +#ifndef JSON_NOEXCEPTION + if (ThrowingKey::inject) + { + throw std::bad_alloc(); + } +#endif + return std::hash {}(k.s); + } +}; +template<> struct equal_to +{ + bool operator()(const ThrowingKey& a, const ThrowingKey& b) const noexcept + { + return a.s == b.s; + } +}; +} // namespace std + +// ---------- aliases ---------- +using OM = nlohmann::ordered_map; +using VP = std::pair; +using OMX = nlohmann::ordered_map; + +TEST_CASE("ordered_map: basic construction, capacity, element access") +{ + OM m; + CHECK(m.empty()); + CHECK(m.size() == 0); + + // operator[] default insert and update + m["a"] = 1; + m["b"] = 2; + CHECK_FALSE(m.empty()); + CHECK(m.size() == 2); + CHECK(m["a"] == 1); + CHECK(m["b"] == 2); + + // overwrite existing + m["a"] = 3; + CHECK(m["a"] == 3); + CHECK(m.size() == 2); + + // at() + CHECK(m.at("b") == 2); +#ifndef JSON_NOEXCEPTION + CHECK_THROWS_AS(m.at("zzz"), std::out_of_range); +#endif + + // initializer-list and range constructor + OM m2{{"x", 7}, {"y", 8}}; + CHECK(m2.size() == 2); + std::vector v{{VP{"q", 1}}, {VP{"w", 2}}}; + OM m3(v.begin(), v.end()); + CHECK(m3.size() == 2); + + // copy / move construction (rebuild_index under the hood) + OM mc(m); + CHECK(mc.size() == m.size()); + CHECK(mc.find("a") != mc.end()); + OM mm(std::move(m2)); + CHECK(mm.size() == 2); + CHECK(mm.find("x") != mm.end()); + CHECK(mm.find("y") != mm.end()); + + // move assignment (uses move + rebuild) + OM mz; + mz = std::move(mm); + CHECK(mz.size() == 2); + CHECK(mz.find("x") != mz.end()); + CHECK(mz.find("y") != mz.end()); + + // const operator[] + const OM& cm = mc; + CHECK(cm["a"] == 3); + + // begin/end iteration order + { + OM seq; + seq["a"] = 1; + seq["b"] = 2; + seq["c"] = 3; + + auto it = seq.begin(); + CHECK(it->first == "a"); + ++it; + CHECK(it->first == "b"); + ++it; + CHECK(it->first == "c"); + ++it; + CHECK(it == seq.end()); + + // const_iterator + const OM& cseq = seq; + auto cit = cseq.begin(); + CHECK(cit->first == "a"); + ++cit; + CHECK(cit->first == "b"); + ++cit; + CHECK(cit->first == "c"); + ++cit; + CHECK(cit == cseq.end()); + + // iterator arithmetic (+ and +=) – O(n), provided for test compatibility + it = seq.begin(); + CHECK((it + 1)->first == "b"); + it += 2; + CHECK(it->first == "c"); + + // reverse iterators + auto rit = seq.rbegin(); + CHECK(rit->first == "c"); + ++rit; + CHECK(rit->first == "b"); + ++rit; + CHECK(rit->first == "a"); + ++rit; + CHECK(rit == seq.rend()); + } +} + +TEST_CASE("ordered_map: insert/emplace, find/count, erase variants, swap/clear, comparisons") +{ + OM om; + + // insert(value) (new key) and duplicate insert + auto p1 = om.insert(VP{"a", 1}); + CHECK(p1.second == true); + CHECK(p1.first->first == "a"); + auto p1b = om.insert(VP{"a", 111}); + CHECK(p1b.second == false); + CHECK(p1b.first->second == 1); + + // insert(rvalue) + auto p2 = om.insert(VP{"b", 2}); + CHECK(p2.second); + CHECK(om.size() == 2); + + // hint overloads (ignored) + auto itb = om.insert(om.begin(), VP{"c", 3}); + CHECK(itb->first == "c"); + CHECK(om.size() == 3); + + // range insert + push_back – also exercises reserve_index_for_range(forward) + std::vector more = {VP{"d", 4}, VP{"e", 5}, VP{"f", 6}}; + om.insert(more.begin(), more.end()); + CHECK(om.size() == 6); + om.push_back(VP{"g", 7}); + CHECK(om.size() == 7); + + // emplace and emplace_hint (hint ignored) + auto e1 = om.emplace(std::string("h"), 8); + CHECK(e1.second); + CHECK(om.size() == 8); + auto eh = om.emplace_hint(om.begin(), std::string("i"), 9); + CHECK(eh->first == "i"); + CHECK(om.size() == 9); + + // find / count + CHECK(om.count("a") == 1); + CHECK(om.find("a") != om.end()); + CHECK(om.count("zzz") == 0); + CHECK(om.find("zzz") == om.end()); + + // erase by key (present / absent) + CHECK(om.erase("b") == 1); + CHECK(om.erase("b") == 0); + CHECK(om.find("b") == om.end()); + + // erase by iterator (returns next) + auto ita = om.find("a"); + auto next = om.erase(ita); + CHECK(next != om.end()); + CHECK(next->first != "a"); + CHECK(om.find("a") == om.end()); + + // erase by const_iterator + const OM& com = om; + auto itc = com.find("c"); + auto after = om.erase(itc); + CHECK(after != om.end()); + CHECK(om.find("c") == om.end()); + + // erase range + auto first = om.begin(); + auto last = om.begin(); + last += 2; // erase two items if present + om.erase(first, last); + // size reduced by two (when possible – map had at least 4 elements here) + CHECK(om.size() >= 3); + + // swap + OM a{{"a", 1}, {"b", 2}}; + OM b{{"a", 1}, {"c", 2}}; + a.swap(b); + CHECK(a.find("c") != a.end()); + CHECK(b.find("b") != b.end()); + + // clear + a.clear(); + CHECK(a.empty()); + + // comparisons (lexicographic on insertion order) + OM x{{"a", 1}, {"b", 2}}; + OM y{{"a", 1}, {"c", 2}}; + CHECK(x != y); + CHECK_UNARY( (x < y) || (y < x) ); // one must be less + CHECK_UNARY( (x <= y) || (y <= x) ); + CHECK_UNARY( (x >= y) || (y >= x) ); +} + +TEST_CASE("ordered_map: strong exception safety on operator[]/insert/emplace (hash throws)") +{ +#ifndef JSON_NOEXCEPTION + OMX om; + om.emplace(ThrowingKey{"ok1"}, 1); + om.emplace(ThrowingKey{"ok2"}, 2); + const auto before_size = om.size(); + + // operator[] path + ThrowingKey::inject = true; + CHECK_THROWS_AS( (void)om[ThrowingKey{"boom"}], std::bad_alloc ); + ThrowingKey::inject = false; + CHECK(om.size() == before_size); + CHECK(om.find(ThrowingKey{"ok1"}) != om.end()); + CHECK(om.find(ThrowingKey{"ok2"}) != om.end()); + CHECK(om.find(ThrowingKey{"boom"}) == om.end()); + + // insert(value) path + ThrowingKey::inject = true; + CHECK_THROWS_AS( (void)om.insert( std::make_pair(ThrowingKey{"boom2"}, 3) ), std::bad_alloc ); + ThrowingKey::inject = false; + CHECK(om.size() == before_size); + CHECK(om.find(ThrowingKey{"boom2"}) == om.end()); + + // emplace path + ThrowingKey::inject = true; + CHECK_THROWS_AS( (void)om.emplace(ThrowingKey{"boom3"}, 4), std::bad_alloc ); + ThrowingKey::inject = false; + CHECK(om.size() == before_size); + CHECK(om.find(ThrowingKey{"boom3"}) == om.end()); +#endif +} + +TEST_CASE("ordered_map: const overloads and reverse iteration") +{ + OM m{{"a", 1}, {"b", 2}, {"c", 3}}; + const OM& cm = m; + + CHECK(cm.find("a") != cm.end()); + CHECK(cm.count("x") == 0); + CHECK(cm.at("b") == 2); + +#ifndef JSON_NOEXCEPTION + CHECK_THROWS_AS(cm.at("nope"), std::out_of_range); +#endif + + // reverse const iteration + auto rcit = cm.rbegin(); + CHECK(rcit->first == "c"); + ++rcit; + CHECK(rcit->first == "b"); + ++rcit; + CHECK(rcit->first == "a"); + ++rcit; + CHECK(rcit == cm.rend()); +}