From f87b70729284823e5f6526c46b48827dcf63c838 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Thu, 15 Oct 2026 08:17:25 +0200 Subject: [PATCH] Move elements when converting rvalue containers to JSON Signed-off-by: Niels Lohmann --- docs/mkdocs/docs/api/basic_json/basic_json.md | 12 +- docs/mkdocs/docs/home/releases.md | 9 + .../nlohmann/detail/conversions/to_json.hpp | 96 +++++++ single_include/nlohmann/json.hpp | 96 +++++++ tests/src/unit-to_json-rvalue.cpp | 257 ++++++++++++++++++ 5 files changed, 469 insertions(+), 1 deletion(-) create mode 100644 tests/src/unit-to_json-rvalue.cpp diff --git a/docs/mkdocs/docs/api/basic_json/basic_json.md b/docs/mkdocs/docs/api/basic_json/basic_json.md index 78fa3560c..e553509ea 100644 --- a/docs/mkdocs/docs/api/basic_json/basic_json.md +++ b/docs/mkdocs/docs/api/basic_json/basic_json.md @@ -68,6 +68,14 @@ basic_json(basic_json&& other) noexcept; `std::unordered_map`, `std::multimap`, and `std::unordered_multimap` with a `key_type` compatible to `string_t` and a `value_type` from which a `basic_json` value can be constructed. - **strings**: `string_t`, string literals, and all compatible string containers can be used. + + !!! note "Rvalue containers are moved from" + + If `val` is an rvalue of a compatible array-like or object-like container other than `array_t`/`object_t` + (e.g., `json j = std::move(vec);` or `json j(std::vector{...});`), the elements are moved into + the JSON value instead of copied. Afterwards, the elements of the source container are in a valid but + unspecified state (**moved-from**). Keys of associative containers are `const` and therefore still copied. + Lvalues and `const` rvalues are copied, and the source is left unchanged. - **numbers**: [`number_integer_t`](number_integer_t.md), [`number_unsigned_t`](number_unsigned_t.md), [`number_float_t`](number_float_t.md), and all convertible number types such as `int`, `size_t`, `int64_t`, `float` or `double` can be used. @@ -236,7 +244,9 @@ basic_json(basic_json&& other) noexcept; 2. No-throw guarantee: this constructor never throws exceptions. 3. Depends on the called constructor. For types directly supported by the library (i.e., all types for which no `to_json()` function was provided), a strong guarantee holds: if an exception is thrown, there are no changes to any - JSON value. + JSON value. **Exception:** when `val` is an rvalue array-like or object-like container, its elements are moved one + by one; if an exception is thrown in the middle of the conversion (e.g., an allocation failure), the JSON value is + unchanged, but the source container may be left partially moved-from. 4. Depends on the called constructor. For types directly supported by the library (i.e., all types for which no `to_json()` function was provided), a strong guarantee holds: if an exception is thrown, there are no changes to any JSON value. diff --git a/docs/mkdocs/docs/home/releases.md b/docs/mkdocs/docs/home/releases.md index fabf95e8a..c0865270e 100644 --- a/docs/mkdocs/docs/home/releases.md +++ b/docs/mkdocs/docs/home/releases.md @@ -9,6 +9,15 @@ published on the [GitHub releases page](https://github.com/nlohmann/json/release This documentation is built from the `develop` branch and may describe changes that are not part of a release yet. Their version numbers are followed by an unreleased badge. +## Unreleased + +- Converting an rvalue container to JSON (`json j = std::move(vec);`, `json j = std::map{...};`) + now **moves** the elements instead of copying them (e.g., `std::vector` with 100,000 strings: 4.7 ms to + 2.3 ms). Two consequences: the source container is left in a valid but unspecified (moved-from) state, and if an + exception is thrown during the conversion, the source may be partially moved-from, so the strong exception guarantee + no longer holds for the source of an rvalue conversion (the JSON value itself is unaffected). Lvalues and `const` + objects are still copied. See [`basic_json`](../api/basic_json/basic_json.md). + ## v3.12.0 (2025-04-11) Fixes bugs found in 3.11.3 and adds several features. All changes are backward-compatible. diff --git a/include/nlohmann/detail/conversions/to_json.hpp b/include/nlohmann/detail/conversions/to_json.hpp index d0d9610f4..b7d2d4bec 100644 --- a/include/nlohmann/detail/conversions/to_json.hpp +++ b/include/nlohmann/detail/conversions/to_json.hpp @@ -167,6 +167,17 @@ struct external_constructor j.replace_value(value_t::array, value); } + /// an rvalue container: move its elements + template + static void construct_moving(BasicJsonType& j, CompatibleArrayType& arr) + { + using std::begin; + using std::end; + + const typename BasicJsonType::json_value value(j.template create(std::make_move_iterator(begin(arr)), std::make_move_iterator(end(arr)))); + j.replace_value(value_t::array, value); + } + template static void construct(BasicJsonType& j, const std::vector& arr) { @@ -217,6 +228,17 @@ struct external_constructor j.replace_value(value_t::object, value); } + /// an rvalue container: move its values + template + static void construct_moving(BasicJsonType& j, CompatibleObjectType& obj) + { + using std::begin; + using std::end; + + const typename BasicJsonType::json_value value(j.template create(std::make_move_iterator(begin(obj)), std::make_move_iterator(end(obj)))); + j.replace_value(value_t::object, value); + } + template < typename BasicJsonType, typename CompatibleObjectType, enable_if_t < !std::is_same::value, int > = 0 > static void construct(BasicJsonType& j, const CompatibleObjectType& obj) @@ -354,6 +376,66 @@ inline void to_json(BasicJsonType& j, const CompatibleArrayType& arr) external_constructor::construct(j, arr); } +// types that have a dedicated to_json overload and must not be treated as generic containers +template +struct has_dedicated_to_json : std::false_type {}; + +#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM +template<> +struct has_dedicated_to_json : std::true_type {}; +#endif + +#if defined(__cpp_lib_char8_t) +template +struct has_dedicated_to_json> : std::true_type {}; +#endif + +#if JSON_HAS_RANGE_VIEW_CONVERSION +template +struct is_range_view_type : is_compatible_range_view {}; +#else +template +struct is_range_view_type : std::false_type {}; +#endif + +#if JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS +template +struct is_enum_keyed_map_type : is_enum_keyed_map {}; +#else +template +struct is_enum_keyed_map_type : std::false_type {}; +#endif + +// rvalue (non-const) container: the generic to_json takes its elements by move +template +using is_movable_container_rvalue = conjunction < + negation>, + negation>, + negation>, + negation>, + negation>, + negation>, + negation>> >; + +// rvalue array-like containers +template +using is_rvalue_array_to_json = conjunction < + is_movable_container_rvalue, + negation>, + negation>, + is_compatible_array_type, + negation>, + negation>, + negation> >; + +// rvalue containers: their elements are moved instead of copied +template < typename BasicJsonType, typename CompatibleArrayType, + enable_if_t < is_rvalue_array_to_json::value, int > = 0 > +inline void to_json(BasicJsonType& j, CompatibleArrayType && arr) +{ + external_constructor::construct_moving(j, arr); +} + #if JSON_HAS_RANGE_VIEW_CONVERSION template < typename BasicJsonType, typename T, enable_if_t < is_compatible_range_view>::value @@ -427,6 +509,20 @@ inline void to_json(BasicJsonType& j, const EnumKeyedMap& map) } #endif +// rvalue object-like containers +template +using is_rvalue_object_to_json = conjunction < + is_movable_container_rvalue, + negation>, + is_compatible_object_type >; + +template < typename BasicJsonType, typename CompatibleObjectType, + enable_if_t < is_rvalue_object_to_json::value, int > = 0 > +inline void to_json(BasicJsonType& j, CompatibleObjectType && obj) +{ + external_constructor::construct_moving(j, obj); +} + template inline void to_json(BasicJsonType& j, typename BasicJsonType::object_t&& obj) { diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 851a8006d..1e6ce1175 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -7140,6 +7140,17 @@ struct external_constructor j.replace_value(value_t::array, value); } + /// an rvalue container: move its elements + template + static void construct_moving(BasicJsonType& j, CompatibleArrayType& arr) + { + using std::begin; + using std::end; + + const typename BasicJsonType::json_value value(j.template create(std::make_move_iterator(begin(arr)), std::make_move_iterator(end(arr)))); + j.replace_value(value_t::array, value); + } + template static void construct(BasicJsonType& j, const std::vector& arr) { @@ -7190,6 +7201,17 @@ struct external_constructor j.replace_value(value_t::object, value); } + /// an rvalue container: move its values + template + static void construct_moving(BasicJsonType& j, CompatibleObjectType& obj) + { + using std::begin; + using std::end; + + const typename BasicJsonType::json_value value(j.template create(std::make_move_iterator(begin(obj)), std::make_move_iterator(end(obj)))); + j.replace_value(value_t::object, value); + } + template < typename BasicJsonType, typename CompatibleObjectType, enable_if_t < !std::is_same::value, int > = 0 > static void construct(BasicJsonType& j, const CompatibleObjectType& obj) @@ -7327,6 +7349,66 @@ inline void to_json(BasicJsonType& j, const CompatibleArrayType& arr) external_constructor::construct(j, arr); } +// types that have a dedicated to_json overload and must not be treated as generic containers +template +struct has_dedicated_to_json : std::false_type {}; + +#if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM +template<> +struct has_dedicated_to_json : std::true_type {}; +#endif + +#if defined(__cpp_lib_char8_t) +template +struct has_dedicated_to_json> : std::true_type {}; +#endif + +#if JSON_HAS_RANGE_VIEW_CONVERSION +template +struct is_range_view_type : is_compatible_range_view {}; +#else +template +struct is_range_view_type : std::false_type {}; +#endif + +#if JSON_USE_OBJECTS_FOR_ENUM_KEYED_MAPS +template +struct is_enum_keyed_map_type : is_enum_keyed_map {}; +#else +template +struct is_enum_keyed_map_type : std::false_type {}; +#endif + +// rvalue (non-const) container: the generic to_json takes its elements by move +template +using is_movable_container_rvalue = conjunction < + negation>, + negation>, + negation>, + negation>, + negation>, + negation>, + negation>> >; + +// rvalue array-like containers +template +using is_rvalue_array_to_json = conjunction < + is_movable_container_rvalue, + negation>, + negation>, + is_compatible_array_type, + negation>, + negation>, + negation> >; + +// rvalue containers: their elements are moved instead of copied +template < typename BasicJsonType, typename CompatibleArrayType, + enable_if_t < is_rvalue_array_to_json::value, int > = 0 > +inline void to_json(BasicJsonType& j, CompatibleArrayType && arr) +{ + external_constructor::construct_moving(j, arr); +} + #if JSON_HAS_RANGE_VIEW_CONVERSION template < typename BasicJsonType, typename T, enable_if_t < is_compatible_range_view>::value @@ -7400,6 +7482,20 @@ inline void to_json(BasicJsonType& j, const EnumKeyedMap& map) } #endif +// rvalue object-like containers +template +using is_rvalue_object_to_json = conjunction < + is_movable_container_rvalue, + negation>, + is_compatible_object_type >; + +template < typename BasicJsonType, typename CompatibleObjectType, + enable_if_t < is_rvalue_object_to_json::value, int > = 0 > +inline void to_json(BasicJsonType& j, CompatibleObjectType && obj) +{ + external_constructor::construct_moving(j, obj); +} + template inline void to_json(BasicJsonType& j, typename BasicJsonType::object_t&& obj) { diff --git a/tests/src/unit-to_json-rvalue.cpp b/tests/src/unit-to_json-rvalue.cpp new file mode 100644 index 000000000..261493f7a --- /dev/null +++ b/tests/src/unit-to_json-rvalue.cpp @@ -0,0 +1,257 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include +using nlohmann::json; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) + #if __has_include() + #include + #define TO_JSON_RVALUE_HAS_FS 1 + #endif +#endif + +namespace +{ +// element type that counts how it is converted to JSON: a conversion from an +// lvalue copies, a conversion from an rvalue moves +struct tracked +{ + std::string payload; + bool moved_from = false; + + explicit tracked(std::string p) : payload(std::move(p)) {} + tracked(const tracked&) = default; + tracked& operator=(const tracked&) = default; + tracked(tracked&& other) noexcept : payload(std::move(other.payload)), moved_from(other.moved_from) + { + other.moved_from = true; + } + tracked& operator=(tracked&& other) noexcept + { + payload = std::move(other.payload); + moved_from = other.moved_from; + other.moved_from = true; + return *this; + } + ~tracked() = default; +}; + +int tracked_copies = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables,misc-use-internal-linkage) +int tracked_moves = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables,misc-use-internal-linkage) + +void to_json(json& j, const tracked& t) +{ + ++tracked_copies; + j = t.payload; +} + +void to_json(json& j, tracked&& t) +{ + ++tracked_moves; + j = std::move(t.payload); + t.moved_from = true; +} + +// user type with its own rvalue-aware serializer +struct udt_container +{ + std::vector values; +}; +} // namespace + +namespace nlohmann +{ +template<> +struct adl_serializer +{ + static void to_json(json& j, const udt_container& c) + { + j = c.values; + j.push_back("copied"); + } + static void to_json(json& j, udt_container&& c) + { + j = std::move(c.values); + j.push_back("moved"); + } +}; +} // namespace nlohmann + +TEST_CASE("to_json from rvalue containers") +{ + const std::string long_a(64, 'a'); + const std::string long_b(64, 'b'); + + SECTION("vector: elements are moved") + { + std::vector v{long_a, long_b}; + const json j = std::move(v); + CHECK(j == json::array({long_a, long_b})); + for (const auto& s : v) // NOLINT(bugprone-use-after-move,hicpp-invalid-access-moved) + { + CHECK(s.empty()); // long strings leave the source element empty when moved + } + } + + SECTION("vector: lvalues are copied and left untouched") + { + const std::vector v{long_a, long_b}; + const json j = v; + CHECK(j == json::array({long_a, long_b})); + CHECK(v[0] == long_a); + CHECK(v[1] == long_b); + + std::vector v2{long_a, long_b}; + const json j2 = v2; + CHECK(j2 == j); + CHECK(v2[0] == long_a); + CHECK(v2[1] == long_b); + } + + SECTION("tracked elements: rvalue moves, lvalue copies") + { + tracked_copies = 0; + tracked_moves = 0; + + std::vector v; + v.emplace_back("x"); + v.emplace_back("y"); + + const json copy = v; + CHECK(copy == json::array({"x", "y"})); + CHECK(tracked_copies == 2); + CHECK(tracked_moves == 0); + CHECK_FALSE(v[0].moved_from); + + const json moved = std::move(v); + CHECK(moved == copy); + CHECK(tracked_copies == 2); + CHECK(tracked_moves == 2); + } + + SECTION("const rvalue is copied") + { + tracked_copies = 0; + tracked_moves = 0; + const std::vector v(2, tracked("z")); + const json j = std::move(v); // NOLINT(performance-move-const-arg,hicpp-move-const-arg) + CHECK(j == json::array({"z", "z"})); + CHECK(tracked_copies == 2); + CHECK(tracked_moves == 0); + } + + SECTION("other sequence containers") + { + CHECK(json(std::deque {long_a, long_b}) == json::array({long_a, long_b})); + CHECK(json(std::list {long_a, long_b}) == json::array({long_a, long_b})); + CHECK(json(std::forward_list {long_a, long_b}) == json::array({long_a, long_b})); + CHECK(json(std::array {{long_a, long_b}}) == json::array({long_a, long_b})); + CHECK(json(std::set {long_a, long_b}) == json::array({long_a, long_b})); + CHECK(json(std::vector {true, false}) == json::array({true, false})); + CHECK(json(std::vector {1, 2, 3}) == json::array({1, 2, 3})); + CHECK(json(std::vector> {{long_a}, {long_b}}) == json::array({json::array({long_a}), json::array({long_b})})); + CHECK(json(std::vector {1, "a"}) == json::array({1, "a"})); + } + + SECTION("valarray") + { + std::valarray va(3); + va[0] = 1; + va[1] = 2; + va[2] = 3; + CHECK(json(std::move(va)) == json::array({1, 2, 3})); // NOLINT(performance-move-const-arg,hicpp-move-const-arg) + CHECK(json(std::valarray(2)) == json::array({0, 0})); + } + + SECTION("array_t and json arrays") + { + json::array_t arr{1, 2}; + CHECK(json(std::move(arr)) == json::array({1, 2})); + json a = json::array({1, 2}); + const json b = std::move(a); + CHECK(b == json::array({1, 2})); + } + + SECTION("object-like containers") + { + std::map m{{"a", long_a}, {"b", long_b}}; + const json j = std::move(m); + CHECK(j == json({{"a", long_a}, {"b", long_b}})); + for (const auto& kv : m) // NOLINT(bugprone-use-after-move,hicpp-invalid-access-moved) + { + CHECK(kv.second.empty()); // values moved, keys (const) copied + } + + const std::map cm{{"a", long_a}}; + const json jc = cm; + CHECK(jc["a"] == long_a); + CHECK(cm.at("a") == long_a); + + std::unordered_map um{{"a", long_a}}; + CHECK(json(std::move(um)) == json({{"a", long_a}})); + + std::multimap mm{{"a", 1}}; + CHECK(json(std::move(mm)) == json({{"a", 1}})); + + tracked_copies = 0; + tracked_moves = 0; + std::map mt; + mt.emplace("k", tracked("v")); + const json jt = std::move(mt); + CHECK(jt == json({{"k", "v"}})); + CHECK(tracked_moves == 1); + CHECK(tracked_copies == 0); + + json::object_t obj{{"a", 1}}; + CHECK(json(std::move(obj)) == json({{"a", 1}})); + } + + SECTION("user type with its own to_json overloads") + { + udt_container c{{1, 2}}; + const json lv = c; + CHECK(lv == json::array({1, 2, "copied"})); + const json rv = std::move(c); + CHECK(rv == json::array({1, 2, "moved"})); + } + + SECTION("assignment and implicit conversion") + { + std::vector v{long_a}; + json j; + j = std::move(v); + CHECK(j == json::array({long_a})); + json k = {std::vector{long_b}}; + CHECK(k.size() == 1); + } + +#ifdef TO_JSON_RVALUE_HAS_FS + SECTION("std::filesystem::path rvalue is still a string") + { + CHECK(json(std::filesystem::path("a/b")) == json("a/b")); +#ifdef __cpp_lib_char8_t + CHECK(json(std::u8string(u8"a/b")) == json("a/b")); +#endif + } +#endif +}