// Glaze Library // For the license information refer to glaze.hpp #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include "glaze/hash/sweethash.hpp" #ifndef GLZ_THROW_OR_ABORT #if __cpp_exceptions #define GLZ_THROW_OR_ABORT(EXC) (throw(EXC)) #else #define GLZ_THROW_OR_ABORT(EXC) (std::abort()) #endif #endif // An ordered_small_map optimized for JSON objects with string keys. // Designed for objects with few keys (typically <256), where preserving // insertion order matters and memory efficiency is important. // Uses 24 bytes on the stack and ~40 bytes per entry on the heap // (32 for the key-value pair + 8 for the hash index) — // significantly less than hash table alternatives. // // Design: // - Preserves insertion order (backed by a contiguous array) // - Linear search for small maps (≤8 entries) — no heap overhead // - Lazily builds a sorted hash index for larger maps (O(log n) lookup) // - Bloom filter accelerates inserts by skipping duplicate checks for new keys namespace glz { template struct ordered_small_map { using key_type = std::string; using mapped_type = T; using value_type = std::pair; using size_type = std::size_t; using difference_type = std::ptrdiff_t; using reference = value_type&; using const_reference = const value_type&; using iterator = value_type*; using const_iterator = const value_type*; using reverse_iterator = std::reverse_iterator; using const_reverse_iterator = std::reverse_iterator; private: // Compact index entry: maps a hash to a position in the data array struct hash_index_entry { uint32_t key_hash; uint32_t index; }; // Bloom filter: 1024 bits (128 bytes) with 2 hash functions. // False positive rate at n entries: (1 - e^(-2n/1024))^2 // n=32: ~4% n=64: ~13% n=128: ~40% n=256: ~74% static constexpr size_t bloom_bytes = 128; static constexpr uint32_t bloom_bits = bloom_bytes * 8; // 1024 static constexpr uint32_t bloom_mask = bloom_bits - 1; // 0x3FF // Heap-allocated index block: [index_header][hash_index_entry × capacity] // The bloom filter is embedded in the header. struct index_header { uint32_t size; // number of data elements covered by the sorted index (0 = fully invalid) uint32_t capacity; // allocated index entry slots uint8_t bloom[bloom_bytes]; // bloom filter for fast insert rejection }; // Compact data storage: pointer + uint32 size/capacity = 16 bytes // Combined with index_ pointer (8 bytes) = 24 bytes total value_type* data_ = nullptr; // 8 bytes uint32_t size_ = 0; // 4 bytes uint32_t capacity_ = 0; // 4 bytes mutable index_header* index_ = nullptr; // 8 bytes // Total: 24 bytes static constexpr size_type linear_search_threshold = 8; static constexpr size_type bloom_threshold = 128; // disable bloom filter above this size static constexpr uint32_t max_u32 = (std::numeric_limits::max)(); static uint32_t hash_key(std::string_view key) noexcept { return sweethash::sweet32(key); } // --- Data array management --- [[noreturn]] static void throw_capacity_overflow() { GLZ_THROW_OR_ABORT(std::length_error("ordered_small_map capacity overflow")); } [[noreturn]] static void throw_index_overflow() { GLZ_THROW_OR_ABORT(std::length_error("ordered_small_map index capacity overflow")); } static uint32_t checked_u32(size_type value) { if (value > static_cast(max_u32)) { throw_capacity_overflow(); } return static_cast(value); } static uint32_t next_data_capacity(uint32_t current) { if (current == 0) return 4; if (current > (max_u32 / 2)) return max_u32; return current * 2; } static uint32_t next_index_capacity(uint32_t current) { if (current == 0) return 16; if (current > (max_u32 / 2)) return max_u32; return current * 2; } static size_t checked_data_allocation_bytes(uint32_t count) { if (count > ((std::numeric_limits::max)() / sizeof(value_type))) { GLZ_THROW_OR_ABORT(std::bad_alloc{}); } return static_cast(count) * sizeof(value_type); } static size_t checked_index_allocation_bytes(uint32_t count) { constexpr size_t header = sizeof(index_header); constexpr size_t entry = sizeof(hash_index_entry); if (count > (((std::numeric_limits::max)() - header) / entry)) { GLZ_THROW_OR_ABORT(std::bad_alloc{}); } return header + static_cast(count) * entry; } static void destroy_range(value_type* data, uint32_t count) noexcept { for (uint32_t i = 0; i < count; ++i) { std::destroy_at(data + i); } } value_type* allocate_and_relocate(uint32_t new_cap) { auto* new_data = static_cast(::operator new(checked_data_allocation_bytes(new_cap))); #if __cpp_exceptions uint32_t constructed = 0; try { for (; constructed < size_; ++constructed) { std::construct_at(new_data + constructed, std::move_if_noexcept(data_[constructed])); } } catch (...) { destroy_range(new_data, constructed); ::operator delete(new_data); throw; } #else for (uint32_t i = 0; i < size_; ++i) { std::construct_at(new_data + i, std::move_if_noexcept(data_[i])); } #endif return new_data; } static value_type* allocate_and_copy(const value_type* source, uint32_t count) { auto* new_data = static_cast(::operator new(checked_data_allocation_bytes(count))); #if __cpp_exceptions uint32_t constructed = 0; try { for (; constructed < count; ++constructed) { std::construct_at(new_data + constructed, source[constructed]); } } catch (...) { destroy_range(new_data, constructed); ::operator delete(new_data); throw; } #else for (uint32_t i = 0; i < count; ++i) { std::construct_at(new_data + i, source[i]); } #endif return new_data; } void reallocate_data(uint32_t new_cap) { if (new_cap < size_) { throw_capacity_overflow(); } auto* new_data = allocate_and_relocate(new_cap); destroy_all(); ::operator delete(data_); data_ = new_data; capacity_ = new_cap; } void grow_if_needed() { if (size_ == capacity_) { if (size_ == max_u32) { throw_capacity_overflow(); } const uint32_t new_cap = next_data_capacity(capacity_); reallocate_data(new_cap); } } void push_back_impl(const value_type& val) { grow_if_needed(); std::construct_at(data_ + size_, val); ++size_; } void push_back_impl(value_type&& val) { grow_if_needed(); std::construct_at(data_ + size_, std::move(val)); ++size_; } template void emplace_back_impl(Args&&... args) { grow_if_needed(); std::construct_at(data_ + size_, std::forward(args)...); ++size_; } template void emplace_back_kv(K&& key, Args&&... args) { grow_if_needed(); std::construct_at(data_ + size_, std::piecewise_construct, std::forward_as_tuple(std::forward(key)), std::forward_as_tuple(std::forward(args)...)); ++size_; } void destroy_all() noexcept { for (uint32_t i = 0; i < size_; ++i) { std::destroy_at(data_ + i); } } void free_data() noexcept { destroy_all(); ::operator delete(data_); data_ = nullptr; size_ = 0; capacity_ = 0; } // --- Bloom filter --- void bloom_set(uint32_t h) const noexcept { const uint32_t a = h & bloom_mask; const uint32_t b = (h >> 10) & bloom_mask; index_->bloom[a >> 3] |= uint8_t(1) << (a & 7); index_->bloom[b >> 3] |= uint8_t(1) << (b & 7); } bool bloom_maybe_contains(uint32_t h) const noexcept { const uint32_t a = h & bloom_mask; const uint32_t b = (h >> 10) & bloom_mask; return (index_->bloom[a >> 3] & (uint8_t(1) << (a & 7))) && (index_->bloom[b >> 3] & (uint8_t(1) << (b & 7))); } void bloom_clear() const noexcept { std::memset(index_->bloom, 0, bloom_bytes); } // --- Index memory management --- hash_index_entry* index_entries() const noexcept { return static_cast(static_cast(index_ + 1)); } uint32_t index_size() const noexcept { return index_ ? index_->size : 0; } void invalidate_index() noexcept { if (index_) index_->size = 0; } void free_index() noexcept { std::free(index_); index_ = nullptr; } void ensure_index_capacity(size_type needed) const { if (needed == 0) return; if (needed > static_cast(max_u32)) { throw_index_overflow(); } const auto needed_u32 = static_cast(needed); if (index_ && index_->capacity >= needed_u32) return; uint32_t cap = index_ ? index_->capacity : 0; while (cap < needed_u32) { const uint32_t next = next_index_capacity(cap); if (next <= cap) { throw_index_overflow(); } cap = next; } auto* block = static_cast(std::realloc(index_, checked_index_allocation_bytes(cap))); if (!block) GLZ_THROW_OR_ABORT(std::bad_alloc{}); if (!index_) { block->size = 0; std::memset(block->bloom, 0, bloom_bytes); } block->capacity = cap; index_ = block; } // --- Index construction --- // Full rebuild: rehash all keys, sort, and repopulate bloom filter. void rebuild_index() const { ensure_index_capacity(static_cast(size_)); bloom_clear(); auto* entries = index_entries(); for (uint32_t i = 0; i < size_; ++i) { const uint32_t h = hash_key(data_[i].first); entries[i] = {h, i}; bloom_set(h); } std::sort(entries, entries + size_, [](const auto& a, const auto& b) { return a.key_hash < b.key_hash; }); index_->size = size_; } // Bring the index up to date. // If fully invalid (after erase) or many entries are stale (bloom deferred), do a full rebuild. // If only a few entries are stale (incremental inserts above bloom_threshold), insert-sort them. void ensure_index() const { if (size_ <= linear_search_threshold) return; const auto current = index_size(); if (current == size_) return; // fully current if (current == 0 || (size_ - current) > linear_search_threshold) { rebuild_index(); // full rebuild return; } // Incrementally insert-sort the few new entries for (uint32_t i = current; i < size_; ++i) { const hash_index_entry entry{hash_key(data_[i].first), i}; const auto* base = index_entries(); const auto* p = branchless_lower_bound(base, index_->size, entry.key_hash); auto pos = static_cast(p - base); const uint32_t m = index_->size; ensure_index_capacity(static_cast(m) + 1); auto* entries = index_entries(); if (pos < m) { std::memmove(entries + pos + 1, entries + pos, (m - pos) * sizeof(hash_index_entry)); } entries[pos] = entry; index_->size = i + 1; } } // Result of a combined find + insertion-point search struct index_find_result { iterator it; // found iterator, or end() if not found size_t insert_pos; // index position for insertion (valid only when it == end()) uint32_t key_hash; // precomputed hash of the key }; // Single binary search that returns both the lookup result and // the index insertion position, avoiding a redundant second search. template index_find_result index_find_or_pos(const K& key, uint32_t h) { ensure_index(); const auto* base = index_entries(); const auto* idx_end = base + index_->size; const auto* p = branchless_lower_bound(base, index_->size, h); const size_t pos = static_cast(p - base); // Scan adjacent entries with matching hash for (auto* scan = p; scan != idx_end && scan->key_hash == h; ++scan) { if (data_[scan->index].first == key) { return {data_ + scan->index, pos, h}; } } return {data_ + size_, pos, h}; } // Linear search - used for small maps template iterator linear_find(const K& key) { for (uint32_t i = 0; i < size_; ++i) { if (data_[i].first == key) return data_ + i; } return data_ + size_; } template const_iterator linear_find(const K& key) const { for (uint32_t i = 0; i < size_; ++i) { if (data_[i].first == key) return data_ + i; } return data_ + size_; } // Branchless binary search - compiles to cmov instead of conditional branches static const hash_index_entry* branchless_lower_bound(const hash_index_entry* p, size_t len, uint32_t target) noexcept { while (len > 1) { size_t half = len / 2; p += (p[half - 1].key_hash < target) * half; len -= half; } // Final element check: advance past it if it's less than target p += (len == 1 && p->key_hash < target); return p; } // Binary search using hash index // On hash match, scans adjacent entries to handle collisions template iterator index_find(const K& key) const { ensure_index(); const uint32_t h = hash_key(key); const auto* base = index_entries(); const auto* idx_end = base + index_->size; const auto* p = branchless_lower_bound(base, index_->size, h); for (; p != idx_end && p->key_hash == h; ++p) { if (data_[p->index].first == key) { return data_ + p->index; } } return data_ + size_; } template const_iterator const_index_find(const K& key) const { ensure_index(); const uint32_t h = hash_key(key); const auto* base = index_entries(); const auto* idx_end = base + index_->size; const auto* p = branchless_lower_bound(base, index_->size, h); for (; p != idx_end && p->key_hash == h; ++p) { if (data_[p->index].first == key) { return data_ + p->index; } } return data_ + size_; } template mapped_type& subscript_or_insert(const K& key, F&& append_fn) { if (size_ <= linear_search_threshold) { auto it = linear_find(key); if (it != end()) return it->second; append_fn(); return data_[size_ - 1].second; } const uint32_t h = hash_key(key); if (try_bloom_insert(h, append_fn)) { return data_[size_ - 1].second; } auto [it, pos, _] = index_find_or_pos(key, h); if (it != end()) return it->second; append_fn(); bloom_set(h); if (index_size() > 0) { const uint32_t n = index_->size; ensure_index_capacity(static_cast(n) + 1); auto* entries = index_entries(); if (pos < n) { std::memmove(entries + pos + 1, entries + pos, (n - pos) * sizeof(hash_index_entry)); } entries[pos] = {h, size_ - 1}; index_->size = size_; } return data_[size_ - 1].second; } // --- Insert helpers --- // Try the bloom filter fast path for insert. // Returns true if the key is definitely new and was appended (caller is done). // Returns false if the bloom says "maybe present" (caller must do full search). template bool try_bloom_insert(uint32_t h, F&& append_fn) { if (index_ && size_ <= bloom_threshold && !bloom_maybe_contains(h)) { // Definitely not present — skip the search append_fn(); bloom_set(h); // Index is now stale (index_->size < size_), will rebuild on next lookup or false positive return true; } return false; } // Full insert path: ensure index, search for duplicate, insert if new. // Returns iterator to existing or newly inserted element, and whether insertion happened. template std::pair indexed_insert(const key_type& key, uint32_t h, F&& append_fn) { auto [it, pos, _] = index_find_or_pos(key, h); if (it != end()) return {it, false}; append_fn(); bloom_set(h); // After ensure_index in index_find_or_pos, index is current for all entries before this one. // Insert the new entry into the sorted index to keep it current. if (index_size() > 0) { const uint32_t n = index_->size; ensure_index_capacity(static_cast(n) + 1); auto* entries = index_entries(); if (pos < n) { std::memmove(entries + pos + 1, entries + pos, (n - pos) * sizeof(hash_index_entry)); } entries[pos] = {h, static_cast(size_ - 1)}; index_->size = size_; } return {data_ + size_ - 1, true}; } public: // Constructors ordered_small_map() = default; ~ordered_small_map() { free_data(); free_index(); } ordered_small_map(std::initializer_list init) { reserve(init.size()); if (init.size() <= linear_search_threshold) { for (const auto& pair : init) { if (linear_find(pair.first) == end()) { push_back_impl(pair); } } } else { for (const auto& pair : init) { insert(pair); } } } template ordered_small_map(InputIt first, InputIt last) { for (; first != last; ++first) { insert(*first); } } ordered_small_map(const ordered_small_map& other) { if (other.size_ > 0) { data_ = allocate_and_copy(other.data_, other.size_); size_ = other.size_; capacity_ = other.size_; } // Don't copy index - it will be rebuilt lazily } ordered_small_map(ordered_small_map&& other) noexcept : data_(other.data_), size_(other.size_), capacity_(other.capacity_), index_(other.index_) { other.data_ = nullptr; other.size_ = 0; other.capacity_ = 0; other.index_ = nullptr; } ordered_small_map& operator=(const ordered_small_map& other) { if (this != &other) { ordered_small_map copy(other); swap(copy); } return *this; } ordered_small_map& operator=(ordered_small_map&& other) noexcept { if (this != &other) { free_data(); free_index(); data_ = other.data_; size_ = other.size_; capacity_ = other.capacity_; index_ = other.index_; other.data_ = nullptr; other.size_ = 0; other.capacity_ = 0; other.index_ = nullptr; } return *this; } // Iterators (follow insertion order) iterator begin() noexcept { return data_; } const_iterator begin() const noexcept { return data_; } const_iterator cbegin() const noexcept { return data_; } iterator end() noexcept { return data_ + size_; } const_iterator end() const noexcept { return data_ + size_; } const_iterator cend() const noexcept { return data_ + size_; } reverse_iterator rbegin() noexcept { return reverse_iterator(end()); } const_reverse_iterator rbegin() const noexcept { return const_reverse_iterator(end()); } const_reverse_iterator crbegin() const noexcept { return const_reverse_iterator(cend()); } reverse_iterator rend() noexcept { return reverse_iterator(begin()); } const_reverse_iterator rend() const noexcept { return const_reverse_iterator(begin()); } const_reverse_iterator crend() const noexcept { return const_reverse_iterator(cbegin()); } // Capacity [[nodiscard]] bool empty() const noexcept { return size_ == 0; } size_type size() const noexcept { return size_; } size_type capacity() const noexcept { return capacity_; } void swap(ordered_small_map& other) noexcept { using std::swap; swap(data_, other.data_); swap(size_, other.size_); swap(capacity_, other.capacity_); swap(index_, other.index_); } friend void swap(ordered_small_map& lhs, ordered_small_map& rhs) noexcept { lhs.swap(rhs); } void reserve(size_type new_cap) { if (new_cap <= capacity_) return; reallocate_data(checked_u32(new_cap)); } void shrink_to_fit() { if (size_ == capacity_) return; if (size_ == 0) { ::operator delete(data_); data_ = nullptr; capacity_ = 0; return; } reallocate_data(size_); } // Modifiers void clear() noexcept { destroy_all(); size_ = 0; free_index(); } std::pair insert(const value_type& value) { if (size_ <= linear_search_threshold) { auto it = linear_find(value.first); if (it != end()) return {it, false}; push_back_impl(value); return {data_ + size_ - 1, true}; } const uint32_t h = hash_key(value.first); if (try_bloom_insert(h, [&] { push_back_impl(value); })) { return {data_ + size_ - 1, true}; } return indexed_insert(value.first, h, [&] { push_back_impl(value); }); } std::pair insert(value_type&& value) { if (size_ <= linear_search_threshold) { auto it = linear_find(value.first); if (it != end()) return {it, false}; push_back_impl(std::move(value)); return {data_ + size_ - 1, true}; } const uint32_t h = hash_key(value.first); if (try_bloom_insert(h, [&] { push_back_impl(std::move(value)); })) { return {data_ + size_ - 1, true}; } // try_bloom_insert returned false without calling the lambda, so value is still valid return indexed_insert(value.first, h, [&] { push_back_impl(std::move(value)); }); } template void insert(InputIt first, InputIt last) { for (; first != last; ++first) { insert(*first); } } void insert(std::initializer_list ilist) { insert(ilist.begin(), ilist.end()); } template std::pair emplace(K&& key, Args&&... args) { return try_emplace(std::forward(key), std::forward(args)...); } template std::pair try_emplace(K&& key, Args&&... args) { if (size_ <= linear_search_threshold) { auto it = linear_find(key); if (it != end()) return {it, false}; emplace_back_kv(std::forward(key), std::forward(args)...); return {data_ + size_ - 1, true}; } const uint32_t h = hash_key(key); if (try_bloom_insert(h, [&] { emplace_back_kv(std::forward(key), std::forward(args)...); })) { return {data_ + size_ - 1, true}; } auto [it, pos, _] = index_find_or_pos(key, h); if (it != end()) return {it, false}; emplace_back_kv(std::forward(key), std::forward(args)...); bloom_set(h); if (index_size() > 0) { const uint32_t n = index_->size; ensure_index_capacity(static_cast(n) + 1); auto* entries = index_entries(); if (pos < n) { std::memmove(entries + pos + 1, entries + pos, (n - pos) * sizeof(hash_index_entry)); } entries[pos] = {h, static_cast(size_ - 1)}; index_->size = size_; } return {data_ + size_ - 1, true}; } template requires(std::is_constructible_v && std::is_assignable_v) std::pair insert_or_assign(const key_type& key, M&& obj) { if (size_ <= linear_search_threshold) { auto it = linear_find(key); if (it != end()) { it->second = std::forward(obj); return {it, false}; } emplace_back_kv(key, std::forward(obj)); return {data_ + size_ - 1, true}; } const uint32_t h = hash_key(key); if (try_bloom_insert(h, [&] { emplace_back_kv(key, std::forward(obj)); })) { return {data_ + size_ - 1, true}; } auto [it, pos, _] = index_find_or_pos(key, h); if (it != end()) { it->second = std::forward(obj); return {it, false}; } emplace_back_kv(key, std::forward(obj)); bloom_set(h); if (index_size() > 0) { const uint32_t n = index_->size; ensure_index_capacity(static_cast(n) + 1); auto* entries = index_entries(); if (pos < n) { std::memmove(entries + pos + 1, entries + pos, (n - pos) * sizeof(hash_index_entry)); } entries[pos] = {h, static_cast(size_ - 1)}; index_->size = size_; } return {data_ + size_ - 1, true}; } template requires(std::is_constructible_v && std::is_assignable_v) std::pair insert_or_assign(key_type&& key, M&& obj) { if (size_ <= linear_search_threshold) { auto it = linear_find(key); if (it != end()) { it->second = std::forward(obj); return {it, false}; } emplace_back_kv(std::move(key), std::forward(obj)); return {data_ + size_ - 1, true}; } const uint32_t h = hash_key(key); if (try_bloom_insert(h, [&] { emplace_back_kv(std::move(key), std::forward(obj)); })) { return {data_ + size_ - 1, true}; } // key may have been moved — but try_bloom_insert returned false, so the lambda didn't execute auto [it, pos, _] = index_find_or_pos(key, h); if (it != end()) { it->second = std::forward(obj); return {it, false}; } emplace_back_kv(std::move(key), std::forward(obj)); bloom_set(h); if (index_size() > 0) { const uint32_t n = index_->size; ensure_index_capacity(static_cast(n) + 1); auto* entries = index_entries(); if (pos < n) { std::memmove(entries + pos + 1, entries + pos, (n - pos) * sizeof(hash_index_entry)); } entries[pos] = {h, static_cast(size_ - 1)}; index_->size = size_; } return {data_ + size_ - 1, true}; } iterator erase(const_iterator pos) { invalidate_index(); auto* mutable_pos = data_ + (pos - data_); std::destroy_at(mutable_pos); // Shift remaining elements left for (auto* p = mutable_pos; p + 1 < data_ + size_; ++p) { std::construct_at(p, std::move(*(p + 1))); std::destroy_at(p + 1); } --size_; return mutable_pos; } iterator erase(const_iterator first, const_iterator last) { if (first == last) return data_ + (first - data_); invalidate_index(); auto* mfirst = data_ + (first - data_); auto* mlast = data_ + (last - data_); const auto count = static_cast(mlast - mfirst); // Destroy erased elements for (auto* p = mfirst; p < mlast; ++p) { std::destroy_at(p); } // Shift remaining elements left for (auto* dst = mfirst; mlast < data_ + size_; ++dst, ++mlast) { std::construct_at(dst, std::move(*mlast)); std::destroy_at(mlast); } size_ -= count; return mfirst; } size_type erase(std::string_view key) { auto it = find(key); if (it != end()) { erase(it); return 1; } return 0; } // Lookup template iterator find(const K& key) { if (size_ <= linear_search_threshold) { return linear_find(key); } return index_find(key); } template const_iterator find(const K& key) const { if (size_ <= linear_search_threshold) { return linear_find(key); } return const_index_find(key); } template bool contains(const K& key) const { return find(key) != end(); } template size_type count(const K& key) const { return contains(key) ? 1 : 0; } // Element access mapped_type& operator[](const key_type& key) { return subscript_or_insert(key, [&] { emplace_back_impl(key, mapped_type{}); }); } mapped_type& operator[](key_type&& key) { // key may have been moved — but only if insertion actually happens. return subscript_or_insert(key, [&] { emplace_back_impl(std::move(key), mapped_type{}); }); } // Heterogeneous lookup version for operator[] // Only participates when K is convertible to string_view but not string template requires(std::convertible_to && !std::same_as, std::string> && !std::same_as, key_type>) mapped_type& operator[](K&& key) { return subscript_or_insert(key, [&] { emplace_back_impl(std::string(std::forward(key)), mapped_type{}); }); } template mapped_type& at(const K& key) { auto it = find(key); if (it == end()) { GLZ_THROW_OR_ABORT(std::out_of_range("ordered_small_map::at: key not found")); } return it->second; } template const mapped_type& at(const K& key) const { auto it = find(key); if (it == end()) { GLZ_THROW_OR_ABORT(std::out_of_range("ordered_small_map::at: key not found")); } return it->second; } // Direct access to underlying data value_type* data() noexcept { return data_; } const value_type* data() const noexcept { return data_; } // Comparison bool operator==(const ordered_small_map& other) const { if (size_ != other.size_) return false; for (uint32_t i = 0; i < size_; ++i) { if (data_[i] != other.data_[i]) return false; } return true; } bool operator!=(const ordered_small_map& other) const { return !(*this == other); } }; }