Files
json/tests/src/unit-ordered_json.cpp
T
Niels Lohmann e486005583 Bound the descent of the copy constructor
basic_json's copy constructor copied objects and arrays by handing the
container to its own copy constructor, which copy-constructs every element
and so reaches this constructor again, once per nesting level. A value
nested deeply enough exhausted the call stack and terminated the process
with a segmentation fault - no exception, nothing the caller could catch.
Parsing such a value works, as the parser is iterative, and so does
destroying one, as #1436 made destruction iterative.

Bound how far the copy descends rather than take the call stack away from
it. The first levels are copied exactly as they were - the containers copy
their own elements, which is by far the fastest way to fill them - and only
once the copy has descended 128 levels is the value below it finished
without the call stack, through an explicit worklist. Copying can therefore
no longer exhaust the stack, however deeply a value is nested, while a value
nested less deeply than the bound - all but a vanishing minority - is copied
by the very same code as before and pays only for one counter.

That counter lives in thread_local storage, as one shared between threads
would be raced. JSON_NO_THREAD_LOCAL switches it off for toolchains without
thread_local; copying then goes through the worklist right away, which
yields the same values but is measurably slower.

The deferred values are completed before the copy they belong to returns, so
a value copied while another copy is going on - by a custom base class, say -
is unaffected by the copy it is nested in.

operator= takes its argument by value, so copy assignment is fixed as well.

Copying is as fast as it was, within measurement noise (medians of 9
interleaved runs, clang -O3): -1.3% for an array of strings, +0.0% for a
flat object, +0.1% for a flat array of numbers, +0.3% for nested arrays,
+0.6% for nested objects and +1.2% for a twitter-like document. Copying a
three-key object costs about ten nanoseconds more, the counter. Deferring
every level instead, rather than only those below the bound, measured
between 3% and 9% slower depending on the shape of the value.

This fixes #5387 for the copy constructor. dump() is still recursive.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-08-20 19:22:16 +02:00

118 lines
3.7 KiB
C++

// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
#include <nlohmann/json.hpp>
using nlohmann::json;
using nlohmann::ordered_json;
TEST_CASE("ordered_json")
{
json j;
ordered_json oj;
j["element3"] = 3;
j["element1"] = 1;
j["element2"] = 2;
oj["element3"] = 3;
oj["element1"] = 1;
oj["element2"] = 2;
CHECK(j.dump() == "{\"element1\":1,\"element2\":2,\"element3\":3}");
CHECK(oj.dump() == "{\"element3\":3,\"element1\":1,\"element2\":2}");
CHECK(j == json(oj));
CHECK(ordered_json(json(oj)) == ordered_json(j));
j.erase("element1");
oj.erase("element1");
CHECK(j.dump() == "{\"element2\":2,\"element3\":3}");
CHECK(oj.dump() == "{\"element3\":3,\"element2\":2}");
// remove again and nothing changes
j.erase("element1");
oj.erase("element1");
CHECK(j.dump() == "{\"element2\":2,\"element3\":3}");
CHECK(oj.dump() == "{\"element3\":3,\"element2\":2}");
// There are no dup keys cause constructor calls emplace...
json const multi {{"z", 1}, {"m", 2}, {"m", 3}, {"y", 4}, {"m", 5}};
CHECK(multi.size() == 3);
CHECK(multi.dump() == "{\"m\":2,\"y\":4,\"z\":1}");
ordered_json multi_ordered {{"z", 1}, {"m", 2}, {"m", 3}, {"y", 4}, {"m", 5}};
CHECK(multi_ordered.size() == 3);
CHECK(multi_ordered.dump() == "{\"z\":1,\"m\":2,\"y\":4}");
CHECK(multi_ordered.erase("m") == 1);
CHECK(multi_ordered.dump() == "{\"z\":1,\"y\":4}");
// Ranged insert test.
// It seems that values shouldn't be overwritten. Only new values are added
json j1 {{"c", 1}, {"b", 2}, {"a", 3}};
const json j2 {{"c", 77}, {"d", 42}, {"a", 4}};
j1.insert( j2.cbegin(), j2.cend() );
CHECK(j1.size() == 4);
CHECK(j1.dump() == "{\"a\":3,\"b\":2,\"c\":1,\"d\":42}");
ordered_json oj1 {{"c", 1}, {"b", 2}, {"a", 3}};
const ordered_json oj2 {{"c", 77}, {"d", 42}, {"a", 4}};
oj1.insert( oj2.cbegin(), oj2.cend() );
CHECK(oj1.size() == 4);
CHECK(oj1.dump() == "{\"c\":1,\"b\":2,\"a\":3,\"d\":42}");
}
TEST_CASE("regression test for issue #3732 - iteration_proxy_value<iter_impl<ordered_json>>")
{
// Naming the proxy type in a function-parameter position forces eager
// instantiation of basic_json<ordered_map>; previously this hit an
// incomplete-type error in set_parents().
auto fn = [](nlohmann::detail::iteration_proxy_value<nlohmann::detail::iter_impl<nlohmann::ordered_json>> const & val)
{
return val.value();
};
static_cast<void>(fn);
}
TEST_CASE("copying an ordered_json with nested values")
{
// ordered_map is backed by a vector, so copying an object that has
// structured values takes a different route than copying a std::map-backed
// one; see https://github.com/nlohmann/json/issues/5387
ordered_json oj;
oj["z"] = 1;
oj["a"]["y"] = 2;
oj["a"]["b"]["x"] = 3;
oj["m"] = {1, 2, {{"w", 4}}};
const ordered_json copy(oj);
SECTION("the copy is equal to the original")
{
CHECK(copy == oj);
CHECK(copy.dump() == oj.dump());
}
SECTION("the key order is preserved at every level")
{
CHECK(copy.dump() == R"({"z":1,"a":{"y":2,"b":{"x":3}},"m":[1,2,{"w":4}]})");
}
SECTION("the copy is independent of the original")
{
ordered_json mutated(oj);
mutated["a"]["b"]["x"] = 99;
CHECK(oj["a"]["b"]["x"] == 3);
CHECK(mutated["a"]["b"]["x"] == 99);
}
}