Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions cmake/ci.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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.
###############################################################################
Expand Down
1 change: 1 addition & 0 deletions docs/mkdocs/docs/api/macros/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
95 changes: 95 additions & 0 deletions docs/mkdocs/docs/api/macros/json_brace_init_copy_semantics.md
Original file line number Diff line number Diff line change
@@ -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 `<nlohmann/json.hpp>`. 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 <nlohmann/json.hpp>

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 <nlohmann/json.hpp>

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.
20 changes: 20 additions & 0 deletions docs/mkdocs/docs/home/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <nlohmann/json.hpp>

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
Expand Down
4 changes: 4 additions & 0 deletions include/nlohmann/detail/macro_scope.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions include/nlohmann/detail/macro_unscope.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions include/nlohmann/json.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<array_t>(init.begin(), init.end());
Expand Down
14 changes: 14 additions & 0 deletions single_include/nlohmann/json.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <compare> // partial_ordering
#endif
Expand Down Expand Up @@ -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<array_t>(init.begin(), init.end());
Expand Down Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions tests/src/unit-regression2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1188,4 +1188,41 @@ TEST_CASE_TEMPLATE("issue #4798 - nlohmann::json::to_msgpack() encode float NaN
CHECK(json::from_cbor(cbor_z_3).get<T>() == -std::numeric_limits<T>::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
Loading