From 62f3b41b30a42663388cf0651024651beef6c222 Mon Sep 17 00:00:00 2001 From: SamareshSingh <97642706+ssam18@users.noreply.github.com> Date: Fri, 8 May 2026 01:20:24 -0500 Subject: [PATCH] fix: treat single-element brace-init as copy/move instead of wrapping in array (#5074) (#5090) * fix: treat single-element brace-init as copy/move When passing a json value using brace initialization with a single element (e.g., `json j{someObj}` or `foo({someJson})`), C++ always prefers the initializer_list constructor over the copy/move constructor. This caused the value to be unexpectedly wrapped in a single-element array. This bug was previously compiler-dependent (GCC wrapped, Clang did not), but Clang 20 started matching GCC behavior, making it a universal issue. Fix: In the initializer_list constructor, when type deduction is enabled and the list has exactly one element, copy/move it directly instead of creating a single-element array. Before: json obj = {{"key", 1}}; json j{obj}; // -> [{"key":1}] (wrong: array) foo({obj}); // -> [{"key":1}] (wrong: array) After: json j{obj}; // -> {"key":1} (correct: copy) foo({obj}); // -> {"key":1} (correct: copy) To explicitly create a single-element array, use json::array({value}). Fixes the issue #5074 Signed-off-by: Samaresh Kumar Singh * fix: regenerate amalgamated single_include/nlohmann/json.hpp - Add missing comment from include/nlohmann/json.hpp explaining the single-element brace-init fix (issue #5074) - Fix extra 4-space indentation in embedded json_fwd.hpp section Regenerated by running: make amalgamate Signed-off-by: Samaresh Kumar Singh * Revert brace-init semantics change and fix amalgamation The single-element brace-init change was a breaking change that cannot be accepted upstream. Reverted all related source, test, and doc changes, then regenerated single_include with correct indentation to pass the amalgamation CI check. Signed-off-by: Samaresh Kumar Singh * Fix: add JSON_BRACE_INIT_COPY_SEMANTICS opt-in macro for issue #5074 Single-element brace initialization wrapping in an array cannot be fixed without breaking existing code. Added JSON_BRACE_INIT_COPY_SEMANTICS as an opt-in macro (default 0) so users can enable copy/move semantics for single-element brace init without affecting anyone relying on the current behavior. Signed-off-by: Samaresh Kumar Singh * docs: add dedicated macro page and CI test target for JSON_BRACE_INIT_COPY_SEMANTICS Signed-off-by: Samaresh Kumar Singh * fix: remove compiler-dependent assertions from #5074 regression test Signed-off-by: Samaresh Kumar Singh * fix: use defined() guard for JSON_BRACE_INIT_COPY_SEMANTICS to satisfy -Wundef Signed-off-by: Samaresh Kumar Singh * docs: fix section name in json_brace_init_copy_semantics.md to pass style check Signed-off-by: Samaresh Kumar Singh * docs: move Default definition section before Notes to fix style check order Signed-off-by: Samaresh Kumar Singh --------- Signed-off-by: Samaresh Kumar Singh --- cmake/ci.cmake | 15 +++ docs/mkdocs/docs/api/macros/index.md | 1 + .../macros/json_brace_init_copy_semantics.md | 95 +++++++++++++++++++ docs/mkdocs/docs/home/faq.md | 20 ++++ include/nlohmann/detail/macro_scope.hpp | 4 + include/nlohmann/detail/macro_unscope.hpp | 1 + include/nlohmann/json.hpp | 9 ++ single_include/nlohmann/json.hpp | 14 +++ tests/src/unit-regression2.cpp | 37 ++++++++ 9 files changed, 196 insertions(+) create mode 100644 docs/mkdocs/docs/api/macros/json_brace_init_copy_semantics.md diff --git a/cmake/ci.cmake b/cmake/ci.cmake index f71136864..559ce8f55 100644 --- a/cmake/ci.cmake +++ b/cmake/ci.cmake @@ -212,6 +212,21 @@ add_custom_target(ci_test_legacycomparison COMMENT "Compile and test with legacy discarded value comparison enabled" ) +############################################################################### +# Enable brace-init copy semantics. +############################################################################### + +add_custom_target(ci_test_brace_init_copy_semantics + COMMAND ${CMAKE_COMMAND} + -DCMAKE_BUILD_TYPE=Debug -GNinja + -DJSON_BuildTests=ON -DJSON_FastTests=ON + -DCMAKE_CXX_FLAGS=-DJSON_BRACE_INIT_COPY_SEMANTICS=1 + -S${PROJECT_SOURCE_DIR} -B${PROJECT_BINARY_DIR}/build_brace_init_copy_semantics + COMMAND ${CMAKE_COMMAND} --build ${PROJECT_BINARY_DIR}/build_brace_init_copy_semantics + COMMAND cd ${PROJECT_BINARY_DIR}/build_brace_init_copy_semantics && ${CMAKE_CTEST_COMMAND} --parallel ${N} --output-on-failure + COMMENT "Compile and test with brace-init copy semantics enabled" +) + ############################################################################### # Disable global UDLs. ############################################################################### diff --git a/docs/mkdocs/docs/api/macros/index.md b/docs/mkdocs/docs/api/macros/index.md index 59e4b3d28..cfe801d43 100644 --- a/docs/mkdocs/docs/api/macros/index.md +++ b/docs/mkdocs/docs/api/macros/index.md @@ -40,6 +40,7 @@ header. See also the [macro overview page](../../features/macros.md). ## Type conversions +- [**JSON_BRACE_INIT_COPY_SEMANTICS**](json_brace_init_copy_semantics.md) - opt in to copy/move semantics for single-element brace initialization - [**JSON_DISABLE_ENUM_SERIALIZATION**](json_disable_enum_serialization.md) - switch off default serialization/deserialization functions for enums - [**JSON_USE_IMPLICIT_CONVERSIONS**](json_use_implicit_conversions.md) - control implicit conversions diff --git a/docs/mkdocs/docs/api/macros/json_brace_init_copy_semantics.md b/docs/mkdocs/docs/api/macros/json_brace_init_copy_semantics.md new file mode 100644 index 000000000..8d88acf47 --- /dev/null +++ b/docs/mkdocs/docs/api/macros/json_brace_init_copy_semantics.md @@ -0,0 +1,95 @@ +# JSON_BRACE_INIT_COPY_SEMANTICS + +```cpp +#define JSON_BRACE_INIT_COPY_SEMANTICS /* value */ +``` + +When defined to `1`, single-element brace initialization of a `basic_json` value is treated as a copy/move of the +element rather than wrapping it in a single-element array. + +## Default definition + +The default value is `0` (disabled — existing behavior is preserved). + +```cpp +#define JSON_BRACE_INIT_COPY_SEMANTICS 0 +``` + +## Notes + +!!! note "Background" + + C++ always prefers the `initializer_list` constructor over the copy/move constructor for brace initialization. This + means that code like + + ```cpp + json obj = {{"key", "value"}}; + json j{obj}; + ``` + + creates a single-element **array** `[{"key":"value"}]` instead of a copy of `obj`. This behavior is + compiler-dependent for older compilers (GCC wrapped, Clang did not), but starting from Clang 20, both compilers + behave the same way. + + Enabling this macro opts into copy/move semantics for this case + (see [#5074](https://github.com/nlohmann/json/issues/5074)). + +!!! warning "Opt-in only" + + This macro must be defined **before** including ``. Defining it after the include has no effect. + +!!! tip "Workaround without the macro" + + To explicitly create a single-element array without enabling this macro, use `json::array()`: + + ```cpp + json j = json::array({obj}); // always creates [obj] + ``` + +## Examples + +??? example "Default behavior (macro not defined)" + + Without the macro, single-element brace initialization wraps the value in an array: + + ```cpp + #include + + using json = nlohmann::json; + + int main() + { + json obj = {{"key", "value"}}; + + json j{obj}; + // j is [{"key":"value"}] -- single-element array, NOT a copy of obj + } + ``` + +??? example "Opt-in copy semantics (macro defined to 1)" + + With the macro, single-element brace initialization copies/moves the value: + + ```cpp + #define JSON_BRACE_INIT_COPY_SEMANTICS 1 + #include + + using json = nlohmann::json; + + int main() + { + json obj = {{"key", "value"}}; + + json j{obj}; + // j is {"key":"value"} -- copy of obj + } + ``` + +## See also + +- [FAQ: Brace initialization yields arrays](../../home/faq.md#brace-initialization-yields-arrays) +- [**basic_json(initializer_list_t)**](../basic_json/basic_json.md) - the affected constructor + +## Version history + +- Added in version 3.12.0. diff --git a/docs/mkdocs/docs/home/faq.md b/docs/mkdocs/docs/home/faq.md index 74709bac6..92cc93581 100644 --- a/docs/mkdocs/docs/home/faq.md +++ b/docs/mkdocs/docs/home/faq.md @@ -38,6 +38,26 @@ for objects. To avoid any confusion and ensure portable code, **do not** use brace initialization with the types `basic_json`, `json`, or `ordered_json` unless you want to create an object or array as shown in the examples above. + To explicitly create a single-element array, use `json::array({value})`: + + ```cpp + json j = json::array({true}); // [true] + ``` + +**Opt-in copy semantics (since version 3.12.0)** + +If you define `JSON_BRACE_INIT_COPY_SEMANTICS` to `1` before including the library, single-element brace initialization is treated as copy/move instead of creating a single-element array: + +```cpp +#define JSON_BRACE_INIT_COPY_SEMANTICS 1 +#include + +json obj = {{"key", "value"}}; +json j{obj}; // -> {"key":"value"} (copy, not array) +``` + +Without the macro (default behavior), `json j{obj}` creates `[{"key":"value"}]`. This opt-in macro fixes issue #5074 while preserving backwards compatibility for existing code. + ## Limitations ### Relaxed parsing diff --git a/include/nlohmann/detail/macro_scope.hpp b/include/nlohmann/detail/macro_scope.hpp index afb400c90..fceebda06 100644 --- a/include/nlohmann/detail/macro_scope.hpp +++ b/include/nlohmann/detail/macro_scope.hpp @@ -599,3 +599,7 @@ #ifndef JSON_USE_GLOBAL_UDLS #define JSON_USE_GLOBAL_UDLS 1 #endif + +#ifndef JSON_BRACE_INIT_COPY_SEMANTICS + #define JSON_BRACE_INIT_COPY_SEMANTICS 0 +#endif diff --git a/include/nlohmann/detail/macro_unscope.hpp b/include/nlohmann/detail/macro_unscope.hpp index af6d8370c..8b9b04459 100644 --- a/include/nlohmann/detail/macro_unscope.hpp +++ b/include/nlohmann/detail/macro_unscope.hpp @@ -26,6 +26,7 @@ #undef JSON_NO_UNIQUE_ADDRESS #undef JSON_DISABLE_ENUM_SERIALIZATION #undef JSON_USE_GLOBAL_UDLS +#undef JSON_BRACE_INIT_COPY_SEMANTICS #ifndef JSON_TEST_KEEP_MACROS #undef JSON_CATCH diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 5ff3e06ed..d52c3b06a 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -955,6 +955,15 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } else { +#if JSON_BRACE_INIT_COPY_SEMANTICS + if (type_deduction && init.size() == 1) + { + *this = init.begin()->moved_or_copied(); + set_parents(); + assert_invariant(); + return; + } +#endif // the initializer list describes an array -> create an array m_data.m_type = value_t::array; m_data.m_value.array = create(init.begin(), init.end()); diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 9fccb456e..0a1ea1d0e 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -2964,6 +2964,10 @@ JSON_HEDLEY_DIAGNOSTIC_POP #define JSON_USE_GLOBAL_UDLS 1 #endif +#ifndef JSON_BRACE_INIT_COPY_SEMANTICS + #define JSON_BRACE_INIT_COPY_SEMANTICS 0 +#endif + #if JSON_HAS_THREE_WAY_COMPARISON #include // partial_ordering #endif @@ -21192,6 +21196,15 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } else { +#if JSON_BRACE_INIT_COPY_SEMANTICS + if (type_deduction && init.size() == 1) + { + *this = init.begin()->moved_or_copied(); + set_parents(); + assert_invariant(); + return; + } +#endif // the initializer list describes an array -> create an array m_data.m_type = value_t::array; m_data.m_value.array = create(init.begin(), init.end()); @@ -25599,6 +25612,7 @@ inline void swap(nlohmann::NLOHMANN_BASIC_JSON_TPL& j1, nlohmann::NLOHMANN_BASIC #undef JSON_NO_UNIQUE_ADDRESS #undef JSON_DISABLE_ENUM_SERIALIZATION #undef JSON_USE_GLOBAL_UDLS +#undef JSON_BRACE_INIT_COPY_SEMANTICS #ifndef JSON_TEST_KEEP_MACROS #undef JSON_CATCH diff --git a/tests/src/unit-regression2.cpp b/tests/src/unit-regression2.cpp index 39dd52378..64deba448 100644 --- a/tests/src/unit-regression2.cpp +++ b/tests/src/unit-regression2.cpp @@ -1203,4 +1203,41 @@ TEST_CASE_TEMPLATE("issue #4798 - nlohmann::json::to_msgpack() encode float NaN CHECK(json::from_cbor(cbor_z_3).get() == -std::numeric_limits::infinity()); } +TEST_CASE("regression test #5074 - portable workaround for single-element brace init") +{ + json const j_obj = {{"key", "value"}}; + + json const j = json::array({j_obj}); + CHECK(j.is_array()); + CHECK(j.size() == 1); + CHECK(j[0] == j_obj); +} + +#if defined(JSON_BRACE_INIT_COPY_SEMANTICS) && (JSON_BRACE_INIT_COPY_SEMANTICS == 1) +TEST_CASE("regression test #5074 - single-element brace init with JSON_BRACE_INIT_COPY_SEMANTICS") +{ + // with JSON_BRACE_INIT_COPY_SEMANTICS: single-element brace init copies/moves + json const j_obj = {{"key", "value"}, {"num", 42}}; + json const j_arr = {1, 2, 3}; + + // object: brace init copies instead of wrapping + json const j1{j_obj}; + CHECK(j1.is_object()); + CHECK(j1 == j_obj); + + // array: brace init copies instead of wrapping + json const j2{j_arr}; + CHECK(j2.is_array()); + CHECK(j2.size() == 3); + CHECK(j2 == j_arr); + + // primitives still work as initializer lists + json const j3{true}; + CHECK(j3.is_boolean()); + + json const j4{42}; + CHECK(j4.is_number_integer()); +} +#endif + DOCTEST_CLANG_SUPPRESS_WARNING_POP