Compare commits

..
Author SHA1 Message Date
Niels Lohmann c5d40f05fe Fix CI: skip std::pair noexcept assumptions on EDG-based compilers
ci_icpc and ci_nvhpc failed to compile unit-ordered_map.cpp: the static
assertion that std::pair<const std::string, ordered_json> is not nothrow
move-constructible fails there. The EDG front end (Intel icpc 2021.10,
NVIDIA nvc++ 25.5) considers the defaulted move constructor of
std::pair<const Key, T> noexcept even if copying Key can throw. With these
compilers, std::vector already moves such elements itself when it grows,
and ordered_map correctly leaves growing to it.

The same misjudgement makes std::vector call std::terminate when a key copy
throws during growth, so the exception-safety test with throwing_key would
abort on these compilers as well.

Skip the static assertion and the exception-safety section when __EDG__ is
defined. Verified with icpc 2021.10 (-std=gnu++11) and nvc++ 25.5 (C++11 and
C++17) on Compiler Explorer: unit-ordered_map and unit-disabled_exceptions
build and pass.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-29 16:44:24 +02:00
Niels Lohmann 998456a218 Merge branch 'develop' into claude/ordered-map-growth-moves-values
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-28 22:22:19 +02:00
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
15 changed files with 586 additions and 253 deletions
+1 -3
View File
@@ -224,7 +224,5 @@ Strong exception safety: if an exception occurs, the original value stays intact
1. Added in version 1.0.0.
2. Added in version 1.0.0.
3. Added in version 3.11.0. Fixed in version 3.13.0 to consistently accept `std::string_view`-convertible keys, as
already supported by [`operator[]`](operator[].md), [`value`](value.md), [`find`](find.md), and other lookup
functions.
3. Added in version 3.11.0.
4. Added in version 2.0.0.
+1 -3
View File
@@ -115,7 +115,5 @@ Logarithmic in the size of the JSON object.
## Version history
1. Added in version 3.11.0.
2. Added in version 3.6.0. Extended template `KeyType` to support comparable types in version 3.11.0. Fixed in
version 3.13.0 to consistently accept `std::string_view`-convertible keys, as already supported by
[`operator[]`](operator[].md), [`at`](at.md), [`value`](value.md), and other lookup functions.
2. Added in version 3.6.0. Extended template `KeyType` to support comparable types in version 3.11.0.
3. Added in version 3.7.0.
+1 -3
View File
@@ -80,6 +80,4 @@ This method always returns `0` when executed on a JSON type that is not an objec
## Version history
1. Added in version 3.11.0.
2. Added in version 1.0.0. Changed parameter `key` type to `KeyType&&` in version 3.11.0. Fixed in version 3.13.0 to
consistently accept `std::string_view`-convertible keys, as already supported by [`operator[]`](operator[].md),
[`at`](at.md), [`value`](value.md), and other lookup functions.
2. Added in version 1.0.0. Changed parameter `key` type to `KeyType&&` in version 3.11.0.
+1 -3
View File
@@ -213,7 +213,5 @@ Strong exception safety: if an exception occurs, the original value stays intact
1. Added in version 1.0.0. Added support for binary types in version 3.8.0.
2. Added in version 1.0.0. Added support for binary types in version 3.8.0.
3. Added in version 1.0.0.
4. Added in version 3.11.0. Fixed in version 3.13.0 to consistently accept `std::string_view`-convertible keys, as
already supported by [`operator[]`](operator[].md), [`at`](at.md), [`value`](value.md), and other lookup
functions.
4. Added in version 3.11.0.
5. Added in version 1.0.0.
+1 -3
View File
@@ -84,6 +84,4 @@ This method always returns `end()` when executed on a JSON type that is not an o
## Version history
1. Added in version 3.11.0.
2. Added in version 1.0.0. Changed to support comparable types in version 3.11.0. Fixed in version 3.13.0 to
consistently accept `std::string_view`-convertible keys, as already supported by [`operator[]`](operator[].md),
[`at`](at.md), [`value`](value.md), and other lookup functions.
2. Added in version 1.0.0. Changed to support comparable types in version 3.11.0.
+1 -3
View File
@@ -185,9 +185,7 @@ changes to any JSON value.
## Version history
1. Added in version 1.0.0. Changed parameter `default_value` type from `const ValueType&` to `ValueType&&` in version 3.11.0.
2. Added in version 3.11.0. Made `ValueType` the first template parameter in version 3.11.2. Fixed in version 3.13.0
to consistently accept `std::string_view`-convertible keys, as already supported by
[`operator[]`](operator[].md), [`at`](at.md), [`find`](find.md), and other lookup functions.
2. Added in version 3.11.0. Made `ValueType` the first template parameter in version 3.11.2.
3. Added in version 2.0.2. Extended to work with arrays in version 3.13.0, including fixing an issue where resolving
`ptr` through an array unexpectedly threw `out_of_range` instead of returning the resolved element (or
`default_value`, as documented).
+11
View File
@@ -28,6 +28,11 @@ A minimal map-like container that preserves insertion order for use within [`nlo
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`](ordered_json.md), 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`)
@@ -56,6 +61,11 @@ std::equal_to<> // since C++14
- **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
@@ -122,3 +132,4 @@ This differs from `#!cpp std::map`, where the same operations are O(log n).
- Added in version 3.9.0 to implement [`nlohmann::ordered_json`](ordered_json.md).
- 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.
+3 -25
View File
@@ -748,30 +748,6 @@ using is_usable_as_key_type = typename std::conditional <
std::true_type,
std::false_type >::type;
#ifdef JSON_HAS_CPP_17
// type trait to check if KeyType can only be used as an object key after
// converting it to std::string_view: it is convertible to std::string_view, the
// object's comparator cannot compare it with object_t::key_type directly, but
// can compare a std::string_view. JSON pointers and JSON iterators are ruled out
// first, so that the conversion checks are never instantiated for them (a JSON
// pointer's deprecated conversion to string_t would be named otherwise).
template < typename BasicJsonType, typename KeyTypeCVRef, typename KeyType = uncvref_t<KeyTypeCVRef>,
bool = is_json_pointer<KeyType>::value || is_json_iterator_of<BasicJsonType, KeyType>::value >
struct is_string_view_convertible_key_type : std::false_type {};
template<typename BasicJsonType, typename KeyTypeCVRef, typename KeyType>
struct is_string_view_convertible_key_type<BasicJsonType, KeyTypeCVRef, KeyType, false>
: std::integral_constant < bool,
std::is_convertible<KeyTypeCVRef, std::string_view>::value
&& !is_usable_as_key_type<typename BasicJsonType::object_comparator_t,
typename BasicJsonType::object_t::key_type, KeyTypeCVRef, true, false>::value
&& is_usable_as_key_type<typename BasicJsonType::object_comparator_t,
typename BasicJsonType::object_t::key_type, std::string_view, true, false>::value > {};
#else
template<typename BasicJsonType, typename KeyTypeCVRef>
struct is_string_view_convertible_key_type : std::false_type {};
#endif
// type trait to check if KeyType can be used as an object key
// true if:
// - KeyType is comparable with BasicJsonType::object_t::key_type
@@ -785,7 +761,9 @@ using is_usable_as_basic_json_key_type = typename std::conditional <
typename BasicJsonType::object_t::key_type, KeyTypeCVRef,
RequireTransparentComparator, ExcludeObjectKeyType>::value
&& !is_json_iterator_of<BasicJsonType, KeyType>::value)
|| is_string_view_convertible_key_type<BasicJsonType, KeyTypeCVRef>::value
#ifdef JSON_HAS_CPP_17
|| std::is_convertible<KeyType, std::string_view>::value
#endif
, std::true_type,
std::false_type >::type;
+12 -32
View File
@@ -805,24 +805,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
return it;
}
/// @brief the key to look up an object member with: the key itself, or its
/// std::string_view if the object can only be searched with that
template < typename KeyType, detail::enable_if_t <
!detail::is_string_view_convertible_key_type<basic_json_t, KeyType>::value, int > = 0 >
static KeyType && lookup_key(KeyType && key) noexcept
{
return std::forward<KeyType>(key);
}
#ifdef JSON_HAS_CPP_17
template < typename KeyType, detail::enable_if_t <
detail::is_string_view_convertible_key_type<basic_json_t, KeyType>::value, int > = 0 >
static std::string_view lookup_key(KeyType && key)
{
return std::forward<KeyType>(key);
}
#endif
/// @brief erase an element from the object and return the following one
/// Not every map returns an iterator from erase(iterator): some containers
/// (e.g., Abseil's hash maps) return void to avoid computing a successor
@@ -2784,7 +2766,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", type_name()), this));
}
auto it = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
auto it = m_data.m_value.object->find(std::forward<KeyType>(key));
if (it == m_data.m_value.object->end())
{
JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(std::forward<KeyType>(key)), "' not found"), this));
@@ -2822,7 +2804,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", type_name()), this));
}
auto it = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
auto it = m_data.m_value.object->find(std::forward<KeyType>(key));
if (it == m_data.m_value.object->end())
{
JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(std::forward<KeyType>(key)), "' not found"), this));
@@ -2958,7 +2940,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
// operator[] only works for objects
if (JSON_HEDLEY_LIKELY(is_object()))
{
auto result = m_data.m_value.object->emplace(lookup_key(std::forward<KeyType>(key)), nullptr);
auto result = m_data.m_value.object->emplace(std::forward<KeyType>(key), nullptr);
return set_parent(result.first->second);
}
@@ -2974,7 +2956,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
// const operator[] only works for objects
if (JSON_HEDLEY_LIKELY(is_object()))
{
auto it = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
auto it = m_data.m_value.object->find(std::forward<KeyType>(key));
JSON_ASSERT(it != m_data.m_value.object->end());
return it->second;
}
@@ -2984,10 +2966,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
private:
template<typename KeyType>
using is_comparable_with_object_key = std::integral_constant < bool,
detail::is_comparable <
object_comparator_t, const typename object_t::key_type&, KeyType >::value
|| detail::is_string_view_convertible_key_type<basic_json_t, KeyType>::value >;
using is_comparable_with_object_key = detail::is_comparable <
object_comparator_t, const typename object_t::key_type&, KeyType >;
template<typename ValueType>
using value_return_type = std::conditional <
@@ -3370,7 +3350,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this));
}
const auto it = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
const auto it = m_data.m_value.object->find(std::forward<KeyType>(key));
if (it != m_data.m_value.object->end())
{
m_data.m_value.object->erase(it);
@@ -3397,7 +3377,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
detail::is_usable_as_basic_json_key_type<basic_json_t, KeyType>::value, int> = 0>
size_type erase(KeyType && key)
{
return erase_internal(lookup_key(std::forward<KeyType>(key)));
return erase_internal(std::forward<KeyType>(key));
}
/// @brief remove element from a JSON array given an index
@@ -3467,7 +3447,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
if (is_object())
{
result.m_it.object_iterator = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
result.m_it.object_iterator = m_data.m_value.object->find(std::forward<KeyType>(key));
}
return result;
@@ -3483,7 +3463,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
if (is_object())
{
result.m_it.object_iterator = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
result.m_it.object_iterator = m_data.m_value.object->find(std::forward<KeyType>(key));
}
return result;
@@ -3506,7 +3486,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
size_type count(KeyType && key) const
{
// return 0 for all nonobject types
return is_object() ? m_data.m_value.object->count(lookup_key(std::forward<KeyType>(key))) : 0;
return is_object() ? m_data.m_value.object->count(std::forward<KeyType>(key)) : 0;
}
/// @brief check the existence of an element in a JSON object
@@ -3524,7 +3504,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
JSON_HEDLEY_WARN_UNUSED_RESULT
bool contains(KeyType && key) const
{
return is_object() && m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key))) != m_data.m_value.object->end();
return is_object() && m_data.m_value.object->find(std::forward<KeyType>(key)) != m_data.m_value.object->end();
}
/// @brief check the existence of an element in a JSON object given a JSON pointer
+65 -5
View File
@@ -8,13 +8,15 @@
#pragma once
#include <algorithm> // max, min
#include <functional> // equal_to, less
#include <initializer_list> // initializer_list
#include <iterator> // input_iterator_tag, iterator_traits
#include <memory> // allocator
#include <stdexcept> // for out_of_range
#include <type_traits> // enable_if, is_convertible
#include <utility> // pair
#include <tuple> // forward_as_tuple
#include <type_traits> // enable_if, integral_constant, is_convertible, is_nothrow_move_constructible
#include <utility> // forward, move, pair, piecewise_construct
#include <vector> // vector
#include <nlohmann/detail/macro_scope.hpp>
@@ -79,7 +81,7 @@ template <class Key, class T, class IgnoredLess = std::less<Key>,
return {it, false};
}
}
Container::emplace_back(key, std::forward<T>(t));
append(key, std::forward<T>(t));
return {std::prev(this->end()), true};
}
@@ -94,7 +96,7 @@ template <class Key, class T, class IgnoredLess = std::less<Key>,
return {it, false};
}
}
Container::emplace_back(std::forward<KeyType>(key), std::forward<T>(t));
append(std::forward<KeyType>(key), std::forward<T>(t));
return {std::prev(this->end()), true};
}
@@ -368,7 +370,7 @@ template <class Key, class T, class IgnoredLess = std::less<Key>,
return {it, false};
}
}
Container::push_back(value);
append(value);
return {--this->end(), true};
}
@@ -386,6 +388,64 @@ template <class Key, class T, class IgnoredLess = std::less<Key>,
}
private:
/*!
@brief add an element whose key is not yet contained at the end
A std::vector copies all elements when it grows, because their const keys
make them not nothrow move constructible. For ordered_json, this is a deep
copy of every value. Where the strong exception guarantee can be kept, grow
the storage here instead, copying only the keys and moving the values.
*/
template<typename... Args>
void append(Args&& ... args)
{
// evaluated here rather than at class scope, because T is still
// incomplete when basic_json instantiates its object_t
using move_values = std::integral_constant<bool, detail::conjunction<
detail::negation<std::is_nothrow_move_constructible<value_type>>,
std::is_copy_constructible<key_type>,
detail::is_default_constructible<mapped_type>,
std::is_nothrow_move_assignable<mapped_type>>::value>;
append_impl(move_values{}, std::forward<Args>(args)...);
}
template<typename... Args>
void append_impl(std::true_type /*unused*/, Args&& ... args)
{
if (this->size() < this->capacity())
{
Container::emplace_back(std::forward<Args>(args)...);
return;
}
// 1. May throw, but only changes tmp: copy the keys, value-initialize
// the values, and add the new element. The arguments may refer to
// elements of this container, so they are used before any value is
// moved out of it.
Container tmp(this->get_allocator()); // equal allocators, so swap() is valid
tmp.reserve((std::min)(this->max_size(), (std::max)(size_type{1}, 2 * this->size())));
for (const auto& element : *this)
{
tmp.emplace_back(std::piecewise_construct, std::forward_as_tuple(element.first), std::forward_as_tuple());
}
tmp.emplace_back(std::forward<Args>(args)...);
// 2. Cannot throw: move the values over and adopt the new storage.
auto it = tmp.begin();
for (auto& element : *this)
{
it->second = std::move(element.second);
++it;
}
Container::swap(tmp);
}
template<typename... Args>
void append_impl(std::false_type /*unused*/, Args&& ... args)
{
Container::emplace_back(std::forward<Args>(args)...);
}
JSON_NO_UNIQUE_ADDRESS key_compare m_compare = key_compare();
};
+80 -62
View File
@@ -4758,30 +4758,6 @@ using is_usable_as_key_type = typename std::conditional <
std::true_type,
std::false_type >::type;
#ifdef JSON_HAS_CPP_17
// type trait to check if KeyType can only be used as an object key after
// converting it to std::string_view: it is convertible to std::string_view, the
// object's comparator cannot compare it with object_t::key_type directly, but
// can compare a std::string_view. JSON pointers and JSON iterators are ruled out
// first, so that the conversion checks are never instantiated for them (a JSON
// pointer's deprecated conversion to string_t would be named otherwise).
template < typename BasicJsonType, typename KeyTypeCVRef, typename KeyType = uncvref_t<KeyTypeCVRef>,
bool = is_json_pointer<KeyType>::value || is_json_iterator_of<BasicJsonType, KeyType>::value >
struct is_string_view_convertible_key_type : std::false_type {};
template<typename BasicJsonType, typename KeyTypeCVRef, typename KeyType>
struct is_string_view_convertible_key_type<BasicJsonType, KeyTypeCVRef, KeyType, false>
: std::integral_constant < bool,
std::is_convertible<KeyTypeCVRef, std::string_view>::value
&& !is_usable_as_key_type<typename BasicJsonType::object_comparator_t,
typename BasicJsonType::object_t::key_type, KeyTypeCVRef, true, false>::value
&& is_usable_as_key_type<typename BasicJsonType::object_comparator_t,
typename BasicJsonType::object_t::key_type, std::string_view, true, false>::value > {};
#else
template<typename BasicJsonType, typename KeyTypeCVRef>
struct is_string_view_convertible_key_type : std::false_type {};
#endif
// type trait to check if KeyType can be used as an object key
// true if:
// - KeyType is comparable with BasicJsonType::object_t::key_type
@@ -4795,7 +4771,9 @@ using is_usable_as_basic_json_key_type = typename std::conditional <
typename BasicJsonType::object_t::key_type, KeyTypeCVRef,
RequireTransparentComparator, ExcludeObjectKeyType>::value
&& !is_json_iterator_of<BasicJsonType, KeyType>::value)
|| is_string_view_convertible_key_type<BasicJsonType, KeyTypeCVRef>::value
#ifdef JSON_HAS_CPP_17
|| std::is_convertible<KeyType, std::string_view>::value
#endif
, std::true_type,
std::false_type >::type;
@@ -25790,13 +25768,15 @@ NLOHMANN_JSON_NAMESPACE_END
#include <algorithm> // max, min
#include <functional> // equal_to, less
#include <initializer_list> // initializer_list
#include <iterator> // input_iterator_tag, iterator_traits
#include <memory> // allocator
#include <stdexcept> // for out_of_range
#include <type_traits> // enable_if, is_convertible
#include <utility> // pair
#include <tuple> // forward_as_tuple
#include <type_traits> // enable_if, integral_constant, is_convertible, is_nothrow_move_constructible
#include <utility> // forward, move, pair, piecewise_construct
#include <vector> // vector
// #include <nlohmann/detail/macro_scope.hpp>
@@ -25863,7 +25843,7 @@ template <class Key, class T, class IgnoredLess = std::less<Key>,
return {it, false};
}
}
Container::emplace_back(key, std::forward<T>(t));
append(key, std::forward<T>(t));
return {std::prev(this->end()), true};
}
@@ -25878,7 +25858,7 @@ template <class Key, class T, class IgnoredLess = std::less<Key>,
return {it, false};
}
}
Container::emplace_back(std::forward<KeyType>(key), std::forward<T>(t));
append(std::forward<KeyType>(key), std::forward<T>(t));
return {std::prev(this->end()), true};
}
@@ -26152,7 +26132,7 @@ template <class Key, class T, class IgnoredLess = std::less<Key>,
return {it, false};
}
}
Container::push_back(value);
append(value);
return {--this->end(), true};
}
@@ -26170,6 +26150,64 @@ template <class Key, class T, class IgnoredLess = std::less<Key>,
}
private:
/*!
@brief add an element whose key is not yet contained at the end
A std::vector copies all elements when it grows, because their const keys
make them not nothrow move constructible. For ordered_json, this is a deep
copy of every value. Where the strong exception guarantee can be kept, grow
the storage here instead, copying only the keys and moving the values.
*/
template<typename... Args>
void append(Args&& ... args)
{
// evaluated here rather than at class scope, because T is still
// incomplete when basic_json instantiates its object_t
using move_values = std::integral_constant<bool, detail::conjunction<
detail::negation<std::is_nothrow_move_constructible<value_type>>,
std::is_copy_constructible<key_type>,
detail::is_default_constructible<mapped_type>,
std::is_nothrow_move_assignable<mapped_type>>::value>;
append_impl(move_values{}, std::forward<Args>(args)...);
}
template<typename... Args>
void append_impl(std::true_type /*unused*/, Args&& ... args)
{
if (this->size() < this->capacity())
{
Container::emplace_back(std::forward<Args>(args)...);
return;
}
// 1. May throw, but only changes tmp: copy the keys, value-initialize
// the values, and add the new element. The arguments may refer to
// elements of this container, so they are used before any value is
// moved out of it.
Container tmp(this->get_allocator()); // equal allocators, so swap() is valid
tmp.reserve((std::min)(this->max_size(), (std::max)(size_type{1}, 2 * this->size())));
for (const auto& element : *this)
{
tmp.emplace_back(std::piecewise_construct, std::forward_as_tuple(element.first), std::forward_as_tuple());
}
tmp.emplace_back(std::forward<Args>(args)...);
// 2. Cannot throw: move the values over and adopt the new storage.
auto it = tmp.begin();
for (auto& element : *this)
{
it->second = std::move(element.second);
++it;
}
Container::swap(tmp);
}
template<typename... Args>
void append_impl(std::false_type /*unused*/, Args&& ... args)
{
Container::emplace_back(std::forward<Args>(args)...);
}
JSON_NO_UNIQUE_ADDRESS key_compare m_compare = key_compare();
};
@@ -26908,24 +26946,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
return it;
}
/// @brief the key to look up an object member with: the key itself, or its
/// std::string_view if the object can only be searched with that
template < typename KeyType, detail::enable_if_t <
!detail::is_string_view_convertible_key_type<basic_json_t, KeyType>::value, int > = 0 >
static KeyType && lookup_key(KeyType && key) noexcept
{
return std::forward<KeyType>(key);
}
#ifdef JSON_HAS_CPP_17
template < typename KeyType, detail::enable_if_t <
detail::is_string_view_convertible_key_type<basic_json_t, KeyType>::value, int > = 0 >
static std::string_view lookup_key(KeyType && key)
{
return std::forward<KeyType>(key);
}
#endif
/// @brief erase an element from the object and return the following one
/// Not every map returns an iterator from erase(iterator): some containers
/// (e.g., Abseil's hash maps) return void to avoid computing a successor
@@ -28887,7 +28907,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", type_name()), this));
}
auto it = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
auto it = m_data.m_value.object->find(std::forward<KeyType>(key));
if (it == m_data.m_value.object->end())
{
JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(std::forward<KeyType>(key)), "' not found"), this));
@@ -28925,7 +28945,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", type_name()), this));
}
auto it = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
auto it = m_data.m_value.object->find(std::forward<KeyType>(key));
if (it == m_data.m_value.object->end())
{
JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(std::forward<KeyType>(key)), "' not found"), this));
@@ -29061,7 +29081,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
// operator[] only works for objects
if (JSON_HEDLEY_LIKELY(is_object()))
{
auto result = m_data.m_value.object->emplace(lookup_key(std::forward<KeyType>(key)), nullptr);
auto result = m_data.m_value.object->emplace(std::forward<KeyType>(key), nullptr);
return set_parent(result.first->second);
}
@@ -29077,7 +29097,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
// const operator[] only works for objects
if (JSON_HEDLEY_LIKELY(is_object()))
{
auto it = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
auto it = m_data.m_value.object->find(std::forward<KeyType>(key));
JSON_ASSERT(it != m_data.m_value.object->end());
return it->second;
}
@@ -29087,10 +29107,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
private:
template<typename KeyType>
using is_comparable_with_object_key = std::integral_constant < bool,
detail::is_comparable <
object_comparator_t, const typename object_t::key_type&, KeyType >::value
|| detail::is_string_view_convertible_key_type<basic_json_t, KeyType>::value >;
using is_comparable_with_object_key = detail::is_comparable <
object_comparator_t, const typename object_t::key_type&, KeyType >;
template<typename ValueType>
using value_return_type = std::conditional <
@@ -29473,7 +29491,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this));
}
const auto it = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
const auto it = m_data.m_value.object->find(std::forward<KeyType>(key));
if (it != m_data.m_value.object->end())
{
m_data.m_value.object->erase(it);
@@ -29500,7 +29518,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
detail::is_usable_as_basic_json_key_type<basic_json_t, KeyType>::value, int> = 0>
size_type erase(KeyType && key)
{
return erase_internal(lookup_key(std::forward<KeyType>(key)));
return erase_internal(std::forward<KeyType>(key));
}
/// @brief remove element from a JSON array given an index
@@ -29570,7 +29588,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
if (is_object())
{
result.m_it.object_iterator = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
result.m_it.object_iterator = m_data.m_value.object->find(std::forward<KeyType>(key));
}
return result;
@@ -29586,7 +29604,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
if (is_object())
{
result.m_it.object_iterator = m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key)));
result.m_it.object_iterator = m_data.m_value.object->find(std::forward<KeyType>(key));
}
return result;
@@ -29609,7 +29627,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
size_type count(KeyType && key) const
{
// return 0 for all nonobject types
return is_object() ? m_data.m_value.object->count(lookup_key(std::forward<KeyType>(key))) : 0;
return is_object() ? m_data.m_value.object->count(std::forward<KeyType>(key)) : 0;
}
/// @brief check the existence of an element in a JSON object
@@ -29627,7 +29645,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
JSON_HEDLEY_WARN_UNUSED_RESULT
bool contains(KeyType && key) const
{
return is_object() && m_data.m_value.object->find(lookup_key(std::forward<KeyType>(key))) != m_data.m_value.object->end();
return is_object() && m_data.m_value.object->find(std::forward<KeyType>(key)) != m_data.m_value.object->end();
}
/// @brief check the existence of an element in a JSON object given a JSON pointer
+33
View File
@@ -119,6 +119,39 @@ BENCHMARK_CAPTURE(ParseIndented, canada / 4, TEST_DATA_DIRECTORY "/nativej
BENCHMARK_CAPTURE(ParseIndented, citm_catalog / 4, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json", 4);
BENCHMARK_CAPTURE(ParseIndented, twitter / 4, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json", 4);
//////////////////////////////////////////////////////////////////////////////
// parse JSON from string into an ordered_json
//
// Same as ParseString above, but with nlohmann::ordered_json, whose objects
// keep their members in a vector: the pair of rows shows what preserving the
// insertion order costs.
//////////////////////////////////////////////////////////////////////////////
static void ParseStringOrdered(benchmark::State& state, const char* filename)
{
std::ifstream f(filename);
std::string str((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
while (state.KeepRunning())
{
state.PauseTiming();
auto* j = new nlohmann::ordered_json();
state.ResumeTiming();
*j = nlohmann::ordered_json::parse(str);
state.PauseTiming();
delete j;
state.ResumeTiming();
}
state.SetBytesProcessed(state.iterations() * str.size());
}
BENCHMARK_CAPTURE(ParseStringOrdered, jeopardy, TEST_DATA_DIRECTORY "/jeopardy/jeopardy.json");
BENCHMARK_CAPTURE(ParseStringOrdered, canada, TEST_DATA_DIRECTORY "/nativejson-benchmark/canada.json");
BENCHMARK_CAPTURE(ParseStringOrdered, citm_catalog, TEST_DATA_DIRECTORY "/nativejson-benchmark/citm_catalog.json");
BENCHMARK_CAPTURE(ParseStringOrdered, twitter, TEST_DATA_DIRECTORY "/nativejson-benchmark/twitter.json");
//////////////////////////////////////////////////////////////////////////////
// serialize JSON
//////////////////////////////////////////////////////////////////////////////
+18
View File
@@ -46,6 +46,24 @@ TEST_CASE("Tests with disabled exceptions")
CHECK(*sax_no_exception::error_string == "[json.exception.parse_error.101] parse error at line 1, column 1: syntax error while parsing value - invalid literal; last read: 'x'");
delete sax_no_exception::error_string; // NOLINT(cppcoreguidelines-owning-memory)
}
SECTION("growing an ordered_json object")
{
auto j = nlohmann::ordered_json::object();
for (int i = 0; i < 100; ++i)
{
j[std::to_string(i)] = {{"nested", i}};
}
CHECK(j.size() == 100);
int i = 0;
for (const auto& element : j.items())
{
CHECK(element.key() == std::to_string(i));
CHECK(element.value()["nested"] == i);
++i;
}
}
}
DOCTEST_GCC_SUPPRESS_WARNING_POP
-111
View File
@@ -1892,115 +1892,4 @@ TEST_CASE("operator[] with user-defined std::string_view-convertible types")
}
}
}
TEST_CASE("keys convertible to std::string_view work with all lookup functions (regression test for #5663)")
{
// a key type convertible only to std::string_view: the case #4958 added
// support for, but only the non-const operator[] compiled with it
struct ViewKey
{
operator std::string_view() const
{
return "a";
}
};
// a key type convertible to both std::string and std::string_view: with
// 3.12.0, such a key worked with at, the const operator[], find, count and
// contains via the conversion to std::string; #4958 made the KeyType&&
// templates win overload resolution for it instead, and those then failed
struct DualKey
{
operator std::string() const
{
return "a";
}
operator std::string_view() const
{
return "a";
}
};
SECTION("nlohmann::json")
{
using json = nlohmann::json;
SECTION("ViewKey")
{
json j = {{"a", 1}};
const json& cj = j;
CHECK(j[ViewKey{}] == 1);
CHECK(cj[ViewKey{}] == 1);
CHECK(j.at(ViewKey{}) == 1);
CHECK(cj.at(ViewKey{}) == 1);
CHECK(j.find(ViewKey{}) != j.end());
CHECK(cj.find(ViewKey{}) != cj.end());
CHECK(j.count(ViewKey{}) == 1);
CHECK(j.contains(ViewKey{}));
CHECK(j.value(ViewKey{}, 0) == 1);
CHECK(j.erase(ViewKey{}) == 1);
CHECK(!j.contains("a"));
}
SECTION("DualKey")
{
json j = {{"a", 1}};
const json& cj = j;
CHECK(j[DualKey{}] == 1);
CHECK(cj[DualKey{}] == 1);
CHECK(j.at(DualKey{}) == 1);
CHECK(cj.at(DualKey{}) == 1);
CHECK(j.find(DualKey{}) != j.end());
CHECK(cj.find(DualKey{}) != cj.end());
CHECK(j.count(DualKey{}) == 1);
CHECK(j.contains(DualKey{}));
CHECK(j.value(DualKey{}, 0) == 1);
CHECK(j.erase(DualKey{}) == 1);
CHECK(!j.contains("a"));
}
}
SECTION("nlohmann::ordered_json")
{
using ordered_json = nlohmann::ordered_json;
SECTION("ViewKey")
{
ordered_json j = {{"a", 1}};
const ordered_json& cj = j;
CHECK(j[ViewKey{}] == 1);
CHECK(cj[ViewKey{}] == 1);
CHECK(j.at(ViewKey{}) == 1);
CHECK(cj.at(ViewKey{}) == 1);
CHECK(j.find(ViewKey{}) != j.end());
CHECK(cj.find(ViewKey{}) != cj.end());
CHECK(j.count(ViewKey{}) == 1);
CHECK(j.contains(ViewKey{}));
CHECK(j.value(ViewKey{}, 0) == 1);
CHECK(j.erase(ViewKey{}) == 1);
CHECK(!j.contains("a"));
}
SECTION("DualKey")
{
ordered_json j = {{"a", 1}};
const ordered_json& cj = j;
CHECK(j[DualKey{}] == 1);
CHECK(cj[DualKey{}] == 1);
CHECK(j.at(DualKey{}) == 1);
CHECK(cj.at(DualKey{}) == 1);
CHECK(j.find(DualKey{}) != j.end());
CHECK(cj.find(DualKey{}) != cj.end());
CHECK(j.count(DualKey{}) == 1);
CHECK(j.contains(DualKey{}));
CHECK(j.value(DualKey{}, 0) == 1);
CHECK(j.erase(DualKey{}) == 1);
CHECK(!j.contains("a"));
}
}
}
#endif
+358
View File
@@ -11,6 +11,97 @@
#include <nlohmann/json.hpp>
using nlohmann::ordered_map;
#include <stdexcept>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
// The EDG front end (Intel icpc, NVIDIA nvc++) considers the defaulted move
// constructor of std::pair<const Key, T> noexcept even if copying Key can
// throw. std::vector then moves such elements itself when it grows (and calls
// std::terminate if a key copy throws), so ordered_map leaves growing to it.
#if defined(__EDG__)
#define JSON_TEST_PAIR_MOVE_IS_NOEXCEPT
#endif
namespace
{
// number of copies made of counted values
int value_copies = 0;
// a mapped type that counts its copies; moving from it leaves -1 behind
struct counted // NOLINT(cppcoreguidelines-special-member-functions,hicpp-special-member-functions)
{
int payload = 0;
counted() = default;
explicit counted(int p) noexcept : payload(p) {}
counted(const counted& other) : payload(other.payload)
{
++value_copies;
}
counted(counted&& other) noexcept : payload(other.payload)
{
other.payload = -1;
}
counted& operator=(const counted&) = delete;
counted& operator=(counted&& other) noexcept
{
payload = other.payload;
other.payload = -1;
return *this;
}
};
#if !defined(JSON_NOEXCEPTION) && !defined(JSON_TEST_PAIR_MOVE_IS_NOEXCEPT)
// number of throwing_key copies that still succeed; the next one throws
// (a negative value means that copies never throw)
int key_copies_until_throw = -1;
// a key type whose copy constructor can be made to throw
struct throwing_key // NOLINT(cppcoreguidelines-special-member-functions,hicpp-special-member-functions)
{
int id = 0;
explicit throwing_key(int i) noexcept : id(i) {}
throwing_key(const throwing_key& other) : id(other.id)
{
if (key_copies_until_throw == 0)
{
throw std::runtime_error("key copy failed");
}
if (key_copies_until_throw > 0)
{
--key_copies_until_throw;
}
}
throwing_key& operator=(const throwing_key&) = delete;
friend bool operator==(const throwing_key& lhs, const throwing_key& rhs) noexcept
{
return lhs.id == rhs.id;
}
};
#endif
// a mapped type that cannot be default-constructed
struct no_default
{
explicit no_default(int v) noexcept : value(v) {}
int value;
};
// ordered_json must keep moving its values when an object grows
using ordered_object_t = nlohmann::ordered_json::object_t;
#if !defined(JSON_TEST_PAIR_MOVE_IS_NOEXCEPT)
static_assert(!std::is_nothrow_move_constructible<ordered_object_t::value_type>::value, "std::vector would move the elements itself");
#endif
static_assert(std::is_copy_constructible<ordered_object_t::key_type>::value, "keys must be copyable");
static_assert(std::is_default_constructible<ordered_object_t::mapped_type>::value, "values must be default-constructible");
static_assert(std::is_nothrow_move_assignable<ordered_object_t::mapped_type>::value, "values must be nothrow move-assignable");
} // namespace
TEST_CASE("ordered_map")
{
SECTION("constructor")
@@ -313,3 +404,270 @@ TEST_CASE("ordered_map")
}
}
}
TEST_CASE("ordered_map growth")
{
SECTION("values are moved, not copied, when the storage grows")
{
ordered_map<std::string, counted> om;
std::size_t growths = 0;
value_copies = 0;
// inserts 100 elements with the given function and counts the growths
const auto fill = [&om, &growths](void (*insert)(ordered_map<std::string, counted>&, int))
{
for (int i = 0; i < 100; ++i)
{
const auto old_capacity = om.capacity();
insert(om, i);
if (om.capacity() > old_capacity)
{
++growths;
}
}
};
// checks that the elements are in insertion order with their values
const auto check_contents = [&om]
{
CHECK(om.size() == 100);
int i = 0;
for (const auto& element : om)
{
CHECK(element.first == std::to_string(i));
CHECK(element.second.payload == i);
++i;
}
};
SECTION("emplace")
{
fill([](ordered_map<std::string, counted>& m, int i)
{
m.emplace(std::to_string(i), counted(i));
});
CHECK(growths >= 3);
CHECK(value_copies == 0);
check_contents();
}
SECTION("operator[]")
{
fill([](ordered_map<std::string, counted>& m, int i)
{
m[std::to_string(i)] = counted(i);
});
CHECK(growths >= 3);
CHECK(value_copies == 0);
check_contents();
}
SECTION("insert(value_type&&)")
{
fill([](ordered_map<std::string, counted>& m, int i)
{
m.insert({std::to_string(i), counted(i)});
});
CHECK(growths >= 3);
CHECK(value_copies == 0);
check_contents();
}
SECTION("insert(const value_type&)")
{
fill([](ordered_map<std::string, counted>& m, int i)
{
const std::pair<const std::string, counted> value(std::to_string(i), counted(i));
m.insert(value);
});
CHECK(growths >= 3);
// only the inserted values are copied
CHECK(value_copies == 100);
check_contents();
}
SECTION("insert(first, last)")
{
std::vector<std::pair<const std::string, counted>> values;
values.reserve(100);
for (int i = 0; i < 100; ++i)
{
values.emplace_back(std::to_string(i), counted(i));
}
value_copies = 0;
om.insert(values.cbegin(), values.cend());
// only the inserted values are copied
CHECK(value_copies == 100);
check_contents();
}
}
SECTION("elements keep their order and values over many growths")
{
ordered_map<std::string, counted> om;
for (int i = 0; i < 1000; ++i)
{
om.emplace(std::to_string(i), counted(i));
}
CHECK(om.size() == 1000);
int i = 0;
for (const auto& element : om)
{
CHECK(element.first == std::to_string(i));
CHECK(element.second.payload == i);
++i;
}
}
SECTION("arguments may refer to elements of the full container")
{
SECTION("moving a value out of the container")
{
ordered_map<std::string, counted> om;
om.reserve(4);
while (om.size() < om.capacity())
{
const auto i = static_cast<int>(om.size());
om.emplace(std::to_string(i), counted(i));
}
const auto size = om.size();
om.emplace("new", std::move(om.at("0")));
CHECK(om.size() == size + 1);
CHECK(om.at("new").payload == 0);
CHECK(om.at("0").payload == -1);
}
SECTION("using a value as key")
{
ordered_map<std::string, std::string> om;
om.reserve(4);
while (om.size() < om.capacity())
{
const auto i = std::to_string(om.size());
om.emplace("k" + i, "v" + i);
}
const auto size = om.size();
om.emplace(om.at("k0"), std::string("x"));
CHECK(om.size() == size + 1);
CHECK(om.at("k0") == "v0");
CHECK(om.at("v0") == "x");
}
SECTION("ordered_json")
{
auto j = nlohmann::ordered_json::object();
auto& object = j.get_ref<nlohmann::ordered_json::object_t&>();
object.reserve(4);
while (object.size() < object.capacity())
{
const auto i = std::to_string(object.size());
j[i] = "a value that is too long for the small string optimization " + i;
}
const auto size = j.size();
j.emplace("new", std::move(j["0"]));
CHECK(j.size() == size + 1);
CHECK(j["new"] == "a value that is too long for the small string optimization 0");
CHECK(j["0"].is_null());
}
}
#if !defined(JSON_NOEXCEPTION) && !defined(JSON_TEST_PAIR_MOVE_IS_NOEXCEPT)
SECTION("the container is unchanged if growing it throws")
{
ordered_map<throwing_key, counted> om;
om.reserve(4);
while (om.size() < om.capacity())
{
const auto i = static_cast<int>(om.size());
om.emplace(throwing_key(i), counted(i));
}
const auto size = om.size();
const auto capacity = om.capacity();
// checks that the elements are unchanged
const auto check_unchanged = [&om, size, capacity]
{
CHECK(om.size() == size);
CHECK(om.capacity() == capacity);
int i = 0;
for (const auto& element : om)
{
CHECK(element.first.id == i);
CHECK(element.second.payload == i);
++i;
}
};
SECTION("emplace")
{
// growing copies the existing keys and then the new one; let each of these copies throw
for (std::size_t k = 0; k <= size; ++k)
{
counted value(100);
key_copies_until_throw = static_cast<int>(k);
CHECK_THROWS_AS(om.emplace(throwing_key(100), std::move(value)), std::runtime_error);
key_copies_until_throw = -1;
check_unchanged();
CHECK(value.payload == 100); // NOLINT(bugprone-use-after-move,hicpp-invalid-access-moved)
}
om.emplace(throwing_key(100), counted(100));
CHECK(om.size() == size + 1);
CHECK(om.capacity() > capacity);
CHECK(om.at(throwing_key(100)).payload == 100);
}
SECTION("insert(const value_type&)")
{
const std::pair<const throwing_key, counted> value(throwing_key(100), counted(100));
value_copies = 0;
key_copies_until_throw = static_cast<int>(size / 2);
CHECK_THROWS_AS(om.insert(value), std::runtime_error);
key_copies_until_throw = -1;
check_unchanged();
CHECK(value_copies == 0);
}
}
#endif
SECTION("elements that std::vector moves, or that cannot be moved back")
{
SECTION("nothrow move-constructible elements")
{
ordered_map<int, counted> om;
value_copies = 0;
for (int i = 0; i < 100; ++i)
{
om.emplace(i, counted(i));
}
CHECK(om.size() == 100);
CHECK(value_copies == 0);
}
SECTION("mapped type without default constructor")
{
ordered_map<std::string, no_default> om;
for (int i = 0; i < 100; ++i)
{
om.emplace(std::to_string(i), no_default(i));
}
CHECK(om.size() == 100);
int i = 0;
for (const auto& element : om)
{
CHECK(element.first == std::to_string(i));
CHECK(element.second.value == i);
++i;
}
}
}
}