Detect a patch stored inside the patched value with JSON_DIAGNOSTICS

patch_inplace applied a copy only when the patch was the value itself. With JSON_DIAGNOSTICS the parent pointers make it cheap to also detect a patch nested inside the value, so that case is handled the same way. Without diagnostics it stays documented undefined behavior.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann committed 2026-10-27 17:18:36 +01:00
1 parent 359c889ed2
commit 95519dbbcb
4 files changed
+41 -8

No files matched your search

@@ -73,11 +73,14 @@ The JSON patch may be the JSON value itself (`#!cpp j.patch_inplace(j)`): the op
the patch made before the first operation is applied, so the result is the same as that of the patch made before the first operation is applied, so the result is the same as that of
`#!cpp j = j.patch(j)`, and the exception safety is the same as above. `#!cpp j = j.patch(j)`, and the exception safety is the same as above.
If [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) is enabled, the same holds for a JSON patch stored inside the
JSON value (e.g., `#!cpp j.patch_inplace(j["patch"])`): it is detected via the parent pointers.
!!! danger "Undefined behavior" !!! danger "Undefined behavior"
The JSON patch must not be a value stored inside the JSON value (e.g., `#!cpp j.patch_inplace(j["patch"])`), as the Without [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md), the JSON patch must not be a value stored inside the
operations may change or destroy it while it is read. Pass a copy (`#!cpp j.patch_inplace(json(j["patch"]))`) or use JSON value (e.g., `#!cpp j.patch_inplace(j["patch"])`), as the operations may change or destroy it while it is
[`patch`](patch.md) instead. read. Pass a copy (`#!cpp j.patch_inplace(json(j["patch"]))`) or use [`patch`](patch.md) instead.
## Examples ## Examples
@@ -132,4 +135,5 @@ the patch made before the first operation is applied, so the result is the same
- Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) instead of - Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) instead of
[`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) for a one-character array index that is [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) for a one-character array index that is
not a digit (e.g., `/x`) in version 3.13.0, as it already did for longer ones. not a digit (e.g., `/x`) in version 3.13.0, as it already did for longer ones.
- Applying a JSON value as patch to itself no longer results in undefined behavior in version 3.13.0. - Applying a JSON value as patch to itself (or, with `JSON_DIAGNOSTICS`, to a value stored inside it) no longer results
in undefined behavior in version 3.13.0.
+10 -2
View File
@@ -6951,8 +6951,16 @@ public:
void patch_inplace(const basic_json& json_patch) void patch_inplace(const basic_json& json_patch)
{ {
// the operations below change *this while iterating json_patch: apply // the operations below change *this while iterating json_patch: apply
// a copy when a value is applied as patch to itself // a copy when a value is applied as patch to itself or, with
if (JSON_HEDLEY_UNLIKELY(&json_patch == this)) // JSON_DIAGNOSTICS, to a value stored inside it
bool patch_aliases_this = (&json_patch == this);
#if JSON_DIAGNOSTICS
for (const basic_json* p = json_patch.m_parent; !patch_aliases_this && p != nullptr; p = p->m_parent)
{
patch_aliases_this = (p == this);
}
#endif
if (JSON_HEDLEY_UNLIKELY(patch_aliases_this))
{ {
// NOLINTNEXTLINE(performance-unnecessary-copy-initialization): *this is modified below // NOLINTNEXTLINE(performance-unnecessary-copy-initialization): *this is modified below
const basic_json json_patch_copy = json_patch; const basic_json json_patch_copy = json_patch;
+10 -2
View File
@@ -43253,8 +43253,16 @@ public:
void patch_inplace(const basic_json& json_patch) void patch_inplace(const basic_json& json_patch)
{ {
// the operations below change *this while iterating json_patch: apply // the operations below change *this while iterating json_patch: apply
// a copy when a value is applied as patch to itself // a copy when a value is applied as patch to itself or, with
if (JSON_HEDLEY_UNLIKELY(&json_patch == this)) // JSON_DIAGNOSTICS, to a value stored inside it
bool patch_aliases_this = (&json_patch == this);
#if JSON_DIAGNOSTICS
for (const basic_json* p = json_patch.m_parent; !patch_aliases_this && p != nullptr; p = p->m_parent)
{
patch_aliases_this = (p == this);
}
#endif
if (JSON_HEDLEY_UNLIKELY(patch_aliases_this))
{ {
// NOLINTNEXTLINE(performance-unnecessary-copy-initialization): *this is modified below // NOLINTNEXTLINE(performance-unnecessary-copy-initialization): *this is modified below
const basic_json json_patch_copy = json_patch; const basic_json json_patch_copy = json_patch;
+13
View File
@@ -855,6 +855,19 @@ TEST_CASE("JSON patch")
CHECK(doc == expected_copies); CHECK(doc == expected_copies);
} }
#if JSON_DIAGNOSTICS
SECTION("patch stored inside the patched value")
{
// the parent pointers of JSON_DIAGNOSTICS make the aliasing detectable
const json op = {{"op", "replace"}, {"path", "/patch"}, {"value", 1}};
json doc = {{"a", 1}, {"patch", json::array({op})}};
const json expected = doc.patch(doc["patch"]);
doc.patch_inplace(doc["patch"]);
CHECK(doc == expected);
}
#endif
SECTION("copy operations with spare capacity") SECTION("copy operations with spare capacity")
{ {
// reserve while the array is empty: reallocating non-empty // reserve while the array is empty: reallocating non-empty