diff --git a/cmake/ci.cmake b/cmake/ci.cmake index f711368649..559ce8f55d 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 59e4b3d280..cfe801d43e 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 0000000000..8d88acf473 --- /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 74709bac68..92cc935810 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 833b7ddf04..3904b66371 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 975d9f0d2d..60ed623163 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 d4502a5c96..92fbd572c6 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 ceb7a9f11d..c9008fe8c1 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 @@ -21074,6 +21078,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()); @@ -25529,6 +25542,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 5ef7da59d4..9a42b765fb 100644 --- a/tests/src/unit-regression2.cpp +++ b/tests/src/unit-regression2.cpp @@ -1188,4 +1188,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