Compare commits

..
Author SHA1 Message Date
Niels Lohmann 07fee4fd55 Exchange the CustomBaseClass subobject in basic_json::swap()
basic_json::swap() (and the friend swap() and the pre-C++20 std::swap
overload that forward to it) only exchanged m_data.m_type/m_data.m_value,
leaving each value's json_base_class_t subobject in place. This is
inconsistent with the copy and move constructors and copy assignment,
which all carry the base class along with the value, so after
a.swap(b) any metadata stored in a CustomBaseClass ended up attached to
the wrong value. Algorithms that mix swap() with moves, such as
std::sort, scrambled the metadata across the whole container.

Fix the member swap() to also exchange the json_base_class_t subobject
and extend the noexcept specifications of swap() and the friend swap()
accordingly.

Fixes #5653.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-29 23:38:56 +02:00
7 changed files with 112 additions and 40 deletions
+12 -4
View File
@@ -6,7 +6,9 @@ void swap(reference other) noexcept (
std::is_nothrow_move_constructible<value_t>::value &&
std::is_nothrow_move_assignable<value_t>::value &&
std::is_nothrow_move_constructible<json_value>::value &&
std::is_nothrow_move_assignable<json_value>::value
std::is_nothrow_move_assignable<json_value>::value &&
std::is_nothrow_move_constructible<json_base_class_t>::value &&
std::is_nothrow_move_assignable<json_base_class_t>::value
);
// (2)
@@ -14,7 +16,9 @@ friend void swap(reference left, reference right) noexcept (
std::is_nothrow_move_constructible<value_t>::value &&
std::is_nothrow_move_assignable<value_t>::value &&
std::is_nothrow_move_constructible<json_value>::value &&
std::is_nothrow_move_assignable<json_value>::value
std::is_nothrow_move_assignable<json_value>::value &&
std::is_nothrow_move_constructible<json_base_class_t>::value &&
std::is_nothrow_move_assignable<json_base_class_t>::value
);
// (3)
@@ -37,11 +41,15 @@ void swap(typename binary_t::container_type& other);
individual elements. All iterators and references remain valid. The past-the-end iterator is invalidated. If macro
[`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) is defined to `#!cpp 1`, the
[`start_pos()`](start_pos.md)/[`end_pos()`](end_pos.md) diagnostic positions are exchanged along with the value.
The [`json_base_class_t`](json_base_class_t.md) subobject is exchanged along with the value as well, the same way it
is copied or moved by the copy/move constructors and assignment operators.
2. Exchanges the contents of the JSON value from `left` with those of `right`. Does not invoke any move, copy, or swap
operations on individual elements. All iterators and references remain valid. The past-the-end iterator is
invalidated. Implemented as a friend function callable via ADL. If macro
[`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) is defined to `#!cpp 1`, the
[`start_pos()`](start_pos.md)/[`end_pos()`](end_pos.md) diagnostic positions are exchanged along with the value.
The [`json_base_class_t`](json_base_class_t.md) subobject is exchanged along with the value as well, the same way it
is copied or moved by the copy/move constructors and assignment operators.
3. Exchanges the contents of a JSON array with those of `other`. Does not invoke any move, copy, or swap operations on
individual elements. All iterators and references remain valid. The past-the-end iterator is invalidated.
4. Exchanges the contents of a JSON object with those of `other`. Does not invoke any move, copy, or swap operations on
@@ -164,8 +172,8 @@ Constant.
## Version history
1. Since version 1.0.0.
2. Since version 1.0.0.
1. Since version 1.0.0. Exchanges the `json_base_class_t` subobject along with the value since version 3.13.0.
2. Since version 1.0.0. Exchanges the `json_base_class_t` subobject along with the value since version 3.13.0.
3. Since version 1.0.0.
4. Since version 1.0.0.
5. Since version 1.0.0.
+1 -1
View File
@@ -336,7 +336,7 @@ void templated_json_throw(ExceptionType exception)
return ej_pair.second == j; \
}); \
if (it != std::end(m)) e = it->first; \
else templated_json_throw<nlohmann::detail::out_of_range>(nlohmann::detail::out_of_range::create(410, nlohmann::detail::concat("enum value out of range for " #ENUM_TYPE ": ", j.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace)), &j)); \
else templated_json_throw<nlohmann::detail::out_of_range>(nlohmann::detail::out_of_range::create(410,"enum value out of range for " #ENUM_TYPE ": " + j.dump(), &j)); \
}
// Ugly macros to avoid uglier copy-paste when specializing basic_json. They
+13 -2
View File
@@ -4335,12 +4335,21 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
std::is_nothrow_move_constructible<value_t>::value&&
std::is_nothrow_move_assignable<value_t>::value&&
std::is_nothrow_move_constructible<json_value>::value&& // NOLINT(cppcoreguidelines-noexcept-swap,performance-noexcept-swap)
std::is_nothrow_move_assignable<json_value>::value
std::is_nothrow_move_assignable<json_value>::value&&
std::is_nothrow_move_constructible<json_base_class_t>::value&&
std::is_nothrow_move_assignable<json_base_class_t>::value
)
{
std::swap(m_data.m_type, other.m_data.m_type);
std::swap(m_data.m_value, other.m_data.m_value);
// the custom base class travels with the value when it is copied or
// moved, so it is exchanged along with it
{
using std::swap;
swap(static_cast<json_base_class_t&>(*this), static_cast<json_base_class_t&>(other));
}
#if JSON_DIAGNOSTIC_POSITIONS
std::swap(start_position, other.start_position);
std::swap(end_position, other.end_position);
@@ -4357,7 +4366,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
std::is_nothrow_move_constructible<value_t>::value&&
std::is_nothrow_move_assignable<value_t>::value&&
std::is_nothrow_move_constructible<json_value>::value&& // NOLINT(cppcoreguidelines-noexcept-swap,performance-noexcept-swap)
std::is_nothrow_move_assignable<json_value>::value
std::is_nothrow_move_assignable<json_value>::value&&
std::is_nothrow_move_constructible<json_base_class_t>::value&&
std::is_nothrow_move_assignable<json_base_class_t>::value
)
{
left.swap(right);
+14 -3
View File
@@ -2747,7 +2747,7 @@ void templated_json_throw(ExceptionType exception)
return ej_pair.second == j; \
}); \
if (it != std::end(m)) e = it->first; \
else templated_json_throw<nlohmann::detail::out_of_range>(nlohmann::detail::out_of_range::create(410, nlohmann::detail::concat("enum value out of range for " #ENUM_TYPE ": ", j.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace)), &j)); \
else templated_json_throw<nlohmann::detail::out_of_range>(nlohmann::detail::out_of_range::create(410,"enum value out of range for " #ENUM_TYPE ": " + j.dump(), &j)); \
}
// Ugly macros to avoid uglier copy-paste when specializing basic_json. They
@@ -30416,12 +30416,21 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
std::is_nothrow_move_constructible<value_t>::value&&
std::is_nothrow_move_assignable<value_t>::value&&
std::is_nothrow_move_constructible<json_value>::value&& // NOLINT(cppcoreguidelines-noexcept-swap,performance-noexcept-swap)
std::is_nothrow_move_assignable<json_value>::value
std::is_nothrow_move_assignable<json_value>::value&&
std::is_nothrow_move_constructible<json_base_class_t>::value&&
std::is_nothrow_move_assignable<json_base_class_t>::value
)
{
std::swap(m_data.m_type, other.m_data.m_type);
std::swap(m_data.m_value, other.m_data.m_value);
// the custom base class travels with the value when it is copied or
// moved, so it is exchanged along with it
{
using std::swap;
swap(static_cast<json_base_class_t&>(*this), static_cast<json_base_class_t&>(other));
}
#if JSON_DIAGNOSTIC_POSITIONS
std::swap(start_position, other.start_position);
std::swap(end_position, other.end_position);
@@ -30438,7 +30447,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
std::is_nothrow_move_constructible<value_t>::value&&
std::is_nothrow_move_assignable<value_t>::value&&
std::is_nothrow_move_constructible<json_value>::value&& // NOLINT(cppcoreguidelines-noexcept-swap,performance-noexcept-swap)
std::is_nothrow_move_assignable<json_value>::value
std::is_nothrow_move_assignable<json_value>::value&&
std::is_nothrow_move_constructible<json_base_class_t>::value&&
std::is_nothrow_move_assignable<json_base_class_t>::value
)
{
left.swap(right);
-24
View File
@@ -174,15 +174,6 @@ bool operator<(const char* op1, const alt_string& op2) noexcept
return op1 < op2.str_impl;
}
enum class alt_color { red, green };
// NOLINTNEXTLINE(misc-use-internal-linkage,misc-const-correctness,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) - false positive
NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(alt_color,
{
{alt_color::red, "red"},
{alt_color::green, "green"},
})
TEST_CASE("alternative string type")
{
SECTION("binary formats")
@@ -383,19 +374,4 @@ TEST_CASE("alternative string type")
const auto j2 = j.flatten();
CHECK(j2.dump() == R"({"/foo/0":"bar","/foo/1":"baz"})");
}
SECTION("strict enum")
{
// regression test for #5667: NLOHMANN_JSON_SERIALIZE_ENUM_STRICT's from_json
// built its exception message with "..." + j.dump(), which does not compile
// when j.dump() returns a custom string_t (here alt_string) instead of
// std::string
alt_json doc;
doc = "red";
CHECK(doc.get<alt_color>() == alt_color::red);
alt_json _;
doc = "blue";
CHECK_THROWS_WITH_AS(_ = doc.get<alt_color>(), "[json.exception.out_of_range.410] enum value out of range for alt_color: \"blue\"", alt_json::out_of_range&);
}
}
-6
View File
@@ -1740,12 +1740,6 @@ TEST_CASE("Strict JSON to enum mapping")
// conversion of unmapped enum -> exception thrown
CHECK_THROWS_WITH_AS(json(strict_cards::andere), "[json.exception.out_of_range.410] enum value out of range for strict_cards", json::out_of_range&);
// invalid UTF-8 -> out_of_range.410, not the type_error.316 thrown while building the
// message (regression test for #5667); such strings can reach get<Enum>() unvalidated,
// e.g. from from_cbor()/from_msgpack() (#5529)
const json j_invalid_utf8 = "\xFF";
CHECK_THROWS_WITH_AS(_ = j_invalid_utf8.get<strict_cards>(), "[json.exception.out_of_range.410] enum value out of range for strict_cards: \"\xEF\xBF\xBD\"", json::out_of_range&);
}
SECTION("traditional enum")
+72
View File
@@ -6,9 +6,11 @@
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include <algorithm>
#include <set>
#include <sstream>
#include <string>
#include <vector>
#include "doctest_compatibility.h"
@@ -180,6 +182,76 @@ TEST_CASE("JSON Node Metadata")
CHECK(val.metadata().at(1) == 2);
}
}
SECTION("member swap")
{
using json = json_with_metadata<int>;
json a = 1;
a.metadata() = 100;
json b = 2;
b.metadata() = 200;
a.swap(b);
CHECK(a.get<int>() == 2);
CHECK(b.get<int>() == 1);
CHECK(a.metadata() == 200);
CHECK(b.metadata() == 100);
}
SECTION("nonmember swap")
{
using json = json_with_metadata<int>;
json a = 1;
a.metadata() = 100;
json b = 2;
b.metadata() = 200;
using std::swap;
swap(a, b);
CHECK(a.get<int>() == 2);
CHECK(b.get<int>() == 1);
CHECK(a.metadata() == 200);
CHECK(b.metadata() == 100);
}
SECTION("std::swap")
{
using json = json_with_metadata<int>;
json a = 1;
a.metadata() = 100;
json b = 2;
b.metadata() = 200;
std::swap(a, b);
CHECK(a.get<int>() == 2);
CHECK(b.get<int>() == 1);
CHECK(a.metadata() == 200);
CHECK(b.metadata() == 100);
}
SECTION("std::sort keeps metadata attached to its value")
{
// std::sort mixes swap() with moves; each value's metadata must
// travel with it, just as it does for copy, move, and assignment
using json = json_with_metadata<int>;
std::vector<json> values;
for (int v :
{
5, 3, 9, 1, 7, 2, 8, 4, 6, 0, 15, 13, 19, 11, 17, 12, 18, 14, 16, 10,
25, 23, 29, 21, 27, 22, 28, 24, 26, 20, 35, 33
})
{
json value = v;
value.metadata() = v;
values.push_back(value);
}
std::sort(values.begin(), values.end());
for (const auto& value : values)
{
CHECK(value.metadata() == value.get<int>());
}
}
}
// Test extending nlohmann::json by using a custom base class.