Compare commits

..
Author SHA1 Message Date
Niels Lohmann 8e4842cbf7 Merge branch 'develop' into copy-scratch-allocator
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 18:07:28 +02:00
Niels Lohmann f386f367df Merge branch 'develop' into copy-scratch-allocator
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 08:24:19 +02:00
Niels Lohmann abf827a5bd Count allocate_at_least in the scratch-counting test allocator
From C++23 on, libc++'s containers allocate through allocate_at_least when
the allocator has one. The test allocator inherited it from std::allocator,
so the scratch allocations were not counted and the test failed on Xcode.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 08:20:13 +02:00
Niels Lohmann a335a81ba7 Allocate the deep copy's key scratch space with the provided allocator
The iterative deep copy builds each object's keys in a temporary vector of
key/value pairs before handing them to the object's range constructor. That
vector holds basic_json values, so like the values themselves it now uses
AllocatorType instead of std::allocator.

Also document that AllocatorType covers the JSON values, while most
temporary storage still uses std::allocator.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-24 22:10:07 +02:00
4 changed files with 85 additions and 3 deletions
@@ -562,7 +562,11 @@ binary32 or binary64 field and have no encoding for `#!cpp long double`.
## `AllocatorType` ## `AllocatorType`
`AllocatorType` is instantiated with **one** argument, for each of `object_t`, `array_t`, `string_t`, `binary_t`, `AllocatorType` is instantiated with **one** argument, for each of `object_t`, `array_t`, `string_t`, `binary_t`,
`basic_json`, and `#!cpp std::pair<const StringType, basic_json>`. `basic_json`, `#!cpp std::pair<const StringType, basic_json>`, and `#!cpp std::pair<StringType, basic_json>`.
`AllocatorType` is not the only allocator a `basic_json` uses. It allocates the JSON values themselves, but most
temporary storage is allocated with `#!cpp std::allocator`. This includes the parser's stacks and the stacks that
process deeply nested values without recursion.
### Always required ### Always required
+2 -1
View File
@@ -1003,7 +1003,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
using copy_worklist_t = std::vector<std::pair<const basic_json*, basic_json*>>; using copy_worklist_t = std::vector<std::pair<const basic_json*, basic_json*>>;
/// scratch space to build the key skeleton of an object copy in one go /// scratch space to build the key skeleton of an object copy in one go
using copy_scratch_t = std::vector<std::pair<typename object_t::key_type, basic_json>>; using copy_scratch_value_t = std::pair<typename object_t::key_type, basic_json>;
using copy_scratch_t = std::vector<copy_scratch_value_t, AllocatorType<copy_scratch_value_t>>;
/// @brief copy everything of @a src into @a dst but its type and value /// @brief copy everything of @a src into @a dst but its type and value
static void copy_metadata(const basic_json& src, basic_json& dst) static void copy_metadata(const basic_json& src, basic_json& dst)
+2 -1
View File
@@ -25648,7 +25648,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
using copy_worklist_t = std::vector<std::pair<const basic_json*, basic_json*>>; using copy_worklist_t = std::vector<std::pair<const basic_json*, basic_json*>>;
/// scratch space to build the key skeleton of an object copy in one go /// scratch space to build the key skeleton of an object copy in one go
using copy_scratch_t = std::vector<std::pair<typename object_t::key_type, basic_json>>; using copy_scratch_value_t = std::pair<typename object_t::key_type, basic_json>;
using copy_scratch_t = std::vector<copy_scratch_value_t, AllocatorType<copy_scratch_value_t>>;
/// @brief copy everything of @a src into @a dst but its type and value /// @brief copy everything of @a src into @a dst but its type and value
static void copy_metadata(const basic_json& src, basic_json& dst) static void copy_metadata(const basic_json& src, basic_json& dst)
+76
View File
@@ -270,6 +270,82 @@ TEST_CASE("controlled bad_alloc")
} }
} }
namespace
{
// counts the allocations of pairs with a non-const first member: the object
// types store std::pair<const Key, T>, so only the scratch space of the
// iterative deep copy allocates std::pair<Key, T>
std::size_t scratch_pair_allocations = 0;
template<class T>
struct is_scratch_pair : std::false_type {};
template<class K, class V>
struct is_scratch_pair<std::pair<K, V>> : std::integral_constant < bool, !std::is_const<K>::value > {};
template<class T>
struct scratch_counting_allocator : std::allocator<T>
{
using std::allocator<T>::allocator;
T* allocate(std::size_t n)
{
if (is_scratch_pair<T>::value)
{
++scratch_pair_allocations;
}
return std::allocator<T>::allocate(n);
}
#ifdef __cpp_lib_allocate_at_least
// std::allocator<T>::allocate_at_least would bypass the counting, and
// libc++'s containers prefer it over allocate from C++23 on
auto allocate_at_least(std::size_t n)
{
if (is_scratch_pair<T>::value)
{
++scratch_pair_allocations;
}
return std::allocator<T>::allocate_at_least(n);
}
#endif
template <class U>
struct rebind
{
using other = scratch_counting_allocator<U>;
};
};
} // namespace
TEST_CASE("deep copy uses the provided allocator")
{
using counting_json = nlohmann::basic_json<std::map,
std::vector,
std::string,
bool,
std::int64_t,
std::uint64_t,
double,
scratch_counting_allocator>;
// deeper than the 128 levels the copy constructor descends into, so the
// innermost objects are copied by the iterative deep copy
counting_json j = 1;
for (std::size_t i = 0; i < 300; ++i)
{
counting_json wrapper = counting_json::object();
wrapper["a"] = std::move(j);
j = std::move(wrapper);
}
scratch_pair_allocations = 0;
// NOLINTNEXTLINE(performance-unnecessary-copy-initialization): the copy is what is tested
const counting_json copy(j);
CHECK(scratch_pair_allocations > 0);
CHECK(copy == j);
}
namespace namespace
{ {
template<class T> template<class T>