Files
json/docs/mkdocs/docs/api/ordered_map.md
T
Niels Lohmann 4b2d2244c3 Move instead of deep-copy ordered_json values when an object grows
ordered_map keeps its elements in a std::vector<std::pair<const Key, T>>.
With a std::string key, that pair is not nothrow move constructible (the
const key has to be copied), so std::vector copies every element when it
reallocates. For ordered_json, this deep-copies every member value an
object already holds, including whole nested subtrees, on each growth
step.

Grow the storage in ordered_map instead, copying the keys and moving the
values. This happens in two phases, so the strong exception guarantee is
kept without try/catch. The first phase may throw, but only touches a
temporary buffer: it copies the keys, value-initializes the values, and
constructs the new element. The second phase moves the values (noexcept)
and swaps the buffers. Because the new element is constructed before any
value is moved, arguments that refer to elements of the container stay
valid, as with std::vector. Types that cannot take this path keep the
std::vector behavior.

Parsing into ordered_json (ParseStringOrdered, Apple M1 Max, clang -O3):
twitter 3.20 -> 1.70 ms, citm_catalog 7.73 -> 3.67 ms, jeopardy 219 ->
177 ms, canada unchanged. The number of allocations for twitter and
citm_catalog drops by two thirds.

Also add ParseStringOrdered rows to the benchmarks, and document the
growth behavior and the exception safety of ordered_map.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-28 18:48:05 +02:00

5.5 KiB
Raw Blame History

nlohmann::ordered_map

template<class Key, class T, class IgnoredLess = std::less<Key>,
         class Allocator = std::allocator<std::pair<const Key, T>>>
struct ordered_map : std::vector<std::pair<const Key, T>, Allocator>;

A minimal map-like container that preserves insertion order for use within nlohmann::ordered_json (nlohmann::basic_json<ordered_map>).

Template parameters

Key
key type
T
mapped type
IgnoredLess
comparison function (ignored and only added to ensure compatibility with #!cpp std::map)
Allocator
allocator type

Iterator invalidation

The type uses a std::vector to store object elements. Therefore, adding elements can yield a reallocation in which case all iterators (including the end() iterator) and all references to the elements are invalidated.

When the storage grows, the keys are copied and the mapped values are moved to the new storage. A plain std::vector would copy the whole elements instead, because their #!cpp const keys make them not nothrow move constructible; for ordered_json, this would be a deep copy of every nested value. The values are only copied if T is not default constructible or not nothrow move assignable.

Member types

  • key_type - key type (Key)
  • mapped_type - mapped type (T)
  • Container - base container type (#!cpp std::vector<std::pair<const Key, T>, Allocator>)
  • iterator
  • const_iterator
  • size_type
  • value_type
  • key_compare - key comparison function
std::equal_to<Key>  // until C++14

std::equal_to<>     // since C++14

Member functions

  • (constructor)
  • (destructor)
  • emplace
  • operator[]
  • at
  • erase
  • count
  • find
  • insert

Exception safety

emplace, operator[], and insert(value) have the strong exception guarantee: if an exception is thrown (for instance, because copying a key or allocating memory fails), the contents of the container are unchanged.

Complexity

Because the elements are stored in a std::vector in insertion order, there is no index to look a key up by. Every key-based operation performs a linear scan over the stored elements. With n denoting the number of elements in the container:

Operation Complexity Note
emplace O(n) scans for an existing key, then appends (amortized O(1))
operator[] O(n) delegates to emplace (non-const) or at (const)
at O(n) throws #!cpp std::out_of_range if the key is not found
find O(n)
count O(n) the result is always 0 or 1
erase(key) O(n) scan, then move the remaining elements one position down
erase(pos), erase(first, last) O(n) moves all elements after the erased range
insert(value) O(n) equivalent to emplace
insert(first, last) O((n + m) * m) for m inserted elements

This differs from #!cpp std::map, where the same operations are O(log n).

!!! warning "Quadratic cost of building large objects"

Because every insertion scans all elements inserted so far, building an object of `n` distinct keys costs
**O(n²)** in total. This applies to filling an [`ordered_json`](ordered_json.md) object key by key as well as to
parsing one, since the parser inserts each key as it is read.

The cost is negligible for the object sizes typically found in configuration files or API payloads, but it grows
steeply for machine-generated objects with many thousands of keys. Measured with `-O2 -DNDEBUG` for parsing a flat
object of `n` keys, relative to `#!cpp nlohmann::json` (which uses `#!cpp std::map`):

| `n`    | `json` | `ordered_json` | factor |
|--------|--------|----------------|--------|
| 2000   | 0.7 ms | 3.6 ms         | 5×     |
| 4000   | 0.8 ms | 14.0 ms        | 19×    |
| 8000   | 1.6 ms | 67.8 ms        | 43×    |
| 16 000 | 3.3 ms | 181.6 ms       | 54×    |

If key order matters for objects of that size, consider a container with a lookup index, such as
[`tsl::ordered_map`](https://github.com/Tessil/ordered-map)
([integration](https://github.com/nlohmann/json/issues/546#issuecomment-304447518)), as the object type -- see
[object order](../features/object_order.md).

Examples

??? example

The example shows the different behavior of `std::map` and `nlohmann::ordered_map`.
 
```cpp
--8<-- "examples/ordered_map.cpp"
```

Output:

```json
--8<-- "examples/ordered_map.output"
```

See also

Version history

  • Added in version 3.9.0 to implement nlohmann::ordered_json.
  • Added key_compare member in version 3.11.0.
  • Changed in version 3.13.0: growing the storage moves the mapped values instead of copying them.