Compare commits

..
Author SHA1 Message Date
Niels Lohmann 93fe62fc0b Assert on missing array indices in const operator[] and document the JSON pointer case
The const operator[] overloads are unchecked by design, and a missing key
or index is undefined behavior. The key overload guards this with a
runtime assertion, but the index overload did not, although the element
access documentation says an assertion fires in both cases. The const
JSON pointer overload inherits both through json_pointer::get_unchecked(),
so a pointer to a missing array index read out of bounds even in debug
builds, and its documentation promised out_of_range.404 for any pointer
that cannot be resolved.

Add JSON_ASSERT(idx < size()) to const operator[](size_type), which also
covers the index leg of the const JSON pointer overload. Document the
undefined behavior for the const JSON pointer overload in operator[].md
and in the runtime assertions page. Release builds are unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 23:17:42 +02:00
8 changed files with 60 additions and 114 deletions
+11 -3
View File
@@ -86,6 +86,9 @@ Strong exception safety: if an exception occurs, the original value stays intact
- Throws [`out_of_range.410`](../../home/exceptions.md#jsonexceptionout_of_range410) if an array index in the passed - Throws [`out_of_range.410`](../../home/exceptions.md#jsonexceptionout_of_range410) if an array index in the passed
JSON pointer `ptr` exceeds the range of `size_type` (e.g., on 32-bit platforms). JSON pointer `ptr` exceeds the range of `size_type` (e.g., on 32-bit platforms).
For the **const** version, an object key or array index in `ptr` that does not exist is not reported by an
exception, but is undefined behavior (see the notes below). Use [`at`](at.md) for checked access.
## Complexity ## Complexity
1. Constant if `idx` is in the range of the array. Otherwise, linear in `idx - size()`. 1. Constant if `idx` is in the range of the array. Otherwise, linear in `idx - size()`.
@@ -100,9 +103,12 @@ Strong exception safety: if an exception occurs, the original value stays intact
The following cases apply to the **const** overloads; the non-const overloads instead insert the missing element The following cases apply to the **const** overloads; the non-const overloads instead insert the missing element
(see the notes below). (see the notes below).
1. If the element at index `idx` does not exist, the behavior is undefined. 1. If the element at index `idx` does not exist, the behavior is undefined and is **guarded by a
[runtime assertion](../../features/assertions.md)**!
2. If the element with key `key` does not exist, the behavior is undefined and is **guarded by a 2. If the element with key `key` does not exist, the behavior is undefined and is **guarded by a
[runtime assertion](../../features/assertions.md)**! [runtime assertion](../../features/assertions.md)**!
3. If the JSON pointer `ptr` refers to an object key or an array index that does not exist, the behavior is
undefined and is **guarded by a [runtime assertion](../../features/assertions.md)**!
1. The non-const version may add values: If `idx` is beyond the range of the array (i.e., `idx >= size()`), then the 1. The non-const version may add values: If `idx` is beyond the range of the array (i.e., `idx >= size()`), then the
array is silently filled up with `#!json null` values to make `idx` a valid reference to the last stored element. In array is silently filled up with `#!json null` values to make `idx` a valid reference to the last stored element. In
@@ -257,9 +263,11 @@ Strong exception safety: if an exception occurs, the original value stays intact
## Version history ## Version history
1. Added in version 1.0.0. 1. Added in version 1.0.0. A missing index in the const version is guarded by a runtime assertion since version
3.13.0.
2. Added in version 1.0.0. Added overloads for `T* key` in version 1.1.0. Removed overloads for `T* key` (replaced by 3) 2. Added in version 1.0.0. Added overloads for `T* key` in version 1.1.0. Removed overloads for `T* key` (replaced by 3)
in version 3.11.0. in version 3.11.0.
3. Added in version 3.11.0. Fixed in version 3.13.0 to consistently accept `std::string_view`-convertible keys, as 3. Added in version 3.11.0. Fixed in version 3.13.0 to consistently accept `std::string_view`-convertible keys, as
already supported by [`at`](at.md), [`value`](value.md), [`find`](find.md), and other lookup functions. already supported by [`at`](at.md), [`value`](value.md), [`find`](find.md), and other lookup functions.
4. Added in version 2.0.0. 4. Added in version 2.0.0. A missing array index in the const version is guarded by a runtime assertion since
version 3.13.0.
@@ -79,7 +79,6 @@ Some important things:
* When using `get<your_type>()`, `your_type` **MUST** be [DefaultConstructible](https://en.cppreference.com/w/cpp/named_req/DefaultConstructible). (There is a way to bypass this requirement described later.) * When using `get<your_type>()`, `your_type` **MUST** be [DefaultConstructible](https://en.cppreference.com/w/cpp/named_req/DefaultConstructible). (There is a way to bypass this requirement described later.)
* In function `from_json`, use function [`at()`](../api/basic_json/at.md) to access the object values rather than `operator[]`. In case a key does not exist, `at` throws an exception that you can handle, whereas `operator[]` exhibits undefined behavior. * In function `from_json`, use function [`at()`](../api/basic_json/at.md) to access the object values rather than `operator[]`. In case a key does not exist, `at` throws an exception that you can handle, whereas `operator[]` exhibits undefined behavior.
* You do not need to add serializers or deserializers for STL types like `std::vector`: the library already implements these. * You do not need to add serializers or deserializers for STL types like `std::vector`: the library already implements these.
* If you control the type, consider defining `to_json`/`from_json` as `friend` functions inside the class ("hidden friends"). Argument-dependent lookup then only finds them for your type, which also avoids a [GCC < 11 compilation error](../home/faq.md#incomplete-detector-type-with-gcc-11).
## Simplify your life with macros ## Simplify your life with macros
+31 -7
View File
@@ -16,14 +16,15 @@ before including the `json.hpp` header.
## Function with runtime assertions ## Function with runtime assertions
### Unchecked object access to a const value ### Unchecked access to a const value
Function [`operator[]`](../api/basic_json/operator%5B%5D.md) implements unchecked access for objects. Whereas a missing Function [`operator[]`](../api/basic_json/operator%5B%5D.md) implements unchecked access for arrays and objects. Whereas
key is added in the case of non-const objects, accessing a const object with a missing key is undefined behavior (think a missing element is added in the case of non-const values, accessing a const value with a missing object key or an
of a dereferenced null pointer) and yields a runtime assertion. invalid array index is undefined behavior (think of a dereferenced null pointer) and yields a runtime assertion. This
also applies to a [JSON pointer](json_pointer.md) that refers to a missing key or an invalid index.
If you are not sure whether an element in an object exists, use checked access with the If you are not sure whether an element exists, use checked access with the [`at` function](../api/basic_json/at.md)
[`at` function](../api/basic_json/at.md) or call the [`contains` function](../api/basic_json/contains.md) before. or call the [`contains` function](../api/basic_json/contains.md) before.
See also the documentation on [element access](element_access/index.md). See also the documentation on [element access](element_access/index.md).
@@ -46,7 +47,30 @@ See also the documentation on [element access](element_access/index.md).
Output: Output:
``` ```
Assertion failed: (m_value.object->find(key) != m_value.object->end()), function operator[], file json.hpp, line 2144. Assertion failed: (it != m_data.m_value.object->end()), function operator[], file json.hpp, line 28795.
```
??? example "Example 2: Invalid array index in a JSON pointer"
The following code will trigger an assertion at runtime:
```cpp
#include <nlohmann/json.hpp>
using json = nlohmann::json;
using namespace nlohmann::literals;
int main()
{
const json j = {{"array", {1, 2, 3}}};
auto v = j["/array/5"_json_pointer];
}
```
Output:
```
Assertion failed: (idx < m_data.m_value.array->size()), function operator[], file json.hpp, line 28758.
``` ```
### Constructing from an uninitialized iterator range ### Constructing from an uninitialized iterator range
-45
View File
@@ -307,51 +307,6 @@ APP_CPPFLAGS += -frtti -fexceptions
The code compiles successfully with [Android NDK](https://developer.android.com/ndk/index.html?hl=ml), Revision 9 - 11 (and possibly later) and [CrystaX's Android NDK](https://www.crystax.net/en/android/ndk) version 10. The code compiles successfully with [Android NDK](https://developer.android.com/ndk/index.html?hl=ml), Revision 9 - 11 (and possibly later) and [CrystaX's Android NDK](https://www.crystax.net/en/android/ndk) version 10.
### Incomplete `detector` type with GCC < 11
!!! question
Why does GCC 10 or older fail with `invalid use of incomplete type 'struct nlohmann::detail::detector<..., to_json_function, ...>'` for a type that holds an `optional` member?
This happens with GCC 10 and older in C++11/C++14 mode when all of these hold:
- a class `Holder` has an `optional<Dummy>` member (e.g., `boost::optional`),
- `Dummy` has a constructor taking a `json` value, and
- `to_json` for `Holder` is a free function in the namespace of `Dummy`.
```cpp
class Dummy {
public:
explicit Dummy(const nlohmann::json& j);
};
class Holder {
boost::optional<Dummy> d;
};
void to_json(nlohmann::json& j, const Holder& h); // triggers the error
```
To decide whether `Dummy` is copyable, the compiler checks whether a `Dummy` can be converted to `json`. That check
looks up `to_json` via argument-dependent lookup, finds the unrelated `to_json` for `Holder`, and eventually asks again
whether `Dummy` is copyable. GCC before version 11 turns this cycle into a hard error; GCC 11 and later, Clang, and
C++17 mode compile the code. The same error shows up without this library whenever a constrained converting constructor
is involved, so the library can't avoid it.
To work around this, define `to_json` (and `from_json`) as a *hidden friend* inside the class. That way,
argument-dependent lookup only finds it for `Holder`:
```cpp
class Holder {
boost::optional<Dummy> d;
friend void to_json(nlohmann::json& j, const Holder& h) { /* ... */ }
};
```
The [`NLOHMANN_DEFINE_TYPE_INTRUSIVE`](../api/macros/nlohmann_define_type_intrusive.md) macros define hidden friends as
well. See [#3669](https://github.com/nlohmann/json/issues/3669) for details.
### Missing STL function ### Missing STL function
!!! question "Questions" !!! question "Questions"
+8 -2
View File
@@ -535,6 +535,10 @@ class json_pointer
@return const reference to the JSON value pointed to by the JSON @return const reference to the JSON value pointed to by the JSON
pointer pointer
@pre Every object key and array index the pointer refers to exists.
Like the const operator[] for keys and indices, a missing one is
undefined behavior, guarded by a runtime assertion.
@throw parse_error.106 if an array index begins with '0' @throw parse_error.106 if an array index begins with '0'
@throw parse_error.109 if an array index was not a number @throw parse_error.109 if an array index was not a number
@throw out_of_range.402 if the array index '-' is used @throw out_of_range.402 if the array index '-' is used
@@ -549,7 +553,8 @@ class json_pointer
{ {
case detail::value_t::object: case detail::value_t::object:
{ {
// use unchecked object access // use unchecked object access; the const operator[]
// asserts that the key exists
ptr = &ptr->operator[](reference_token); ptr = &ptr->operator[](reference_token);
break; break;
} }
@@ -562,7 +567,8 @@ class json_pointer
JSON_THROW(detail::out_of_range::create(402, detail::concat("array index '-' (", std::to_string(ptr->m_data.m_value.array->size()), ") is out of range"), ptr)); JSON_THROW(detail::out_of_range::create(402, detail::concat("array index '-' (", std::to_string(ptr->m_data.m_value.array->size()), ") is out of range"), ptr));
} }
// use unchecked array access // use unchecked array access; the const operator[]
// asserts that the index exists
ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token)); ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token));
break; break;
} }
+1
View File
@@ -2866,6 +2866,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
// const operator[] only works for arrays // const operator[] only works for arrays
if (JSON_HEDLEY_LIKELY(is_array())) if (JSON_HEDLEY_LIKELY(is_array()))
{ {
JSON_ASSERT(idx < m_data.m_value.array->size());
return m_data.m_value.array->operator[](idx); return m_data.m_value.array->operator[](idx);
} }
+9 -2
View File
@@ -19179,6 +19179,10 @@ class json_pointer
@return const reference to the JSON value pointed to by the JSON @return const reference to the JSON value pointed to by the JSON
pointer pointer
@pre Every object key and array index the pointer refers to exists.
Like the const operator[] for keys and indices, a missing one is
undefined behavior, guarded by a runtime assertion.
@throw parse_error.106 if an array index begins with '0' @throw parse_error.106 if an array index begins with '0'
@throw parse_error.109 if an array index was not a number @throw parse_error.109 if an array index was not a number
@throw out_of_range.402 if the array index '-' is used @throw out_of_range.402 if the array index '-' is used
@@ -19193,7 +19197,8 @@ class json_pointer
{ {
case detail::value_t::object: case detail::value_t::object:
{ {
// use unchecked object access // use unchecked object access; the const operator[]
// asserts that the key exists
ptr = &ptr->operator[](reference_token); ptr = &ptr->operator[](reference_token);
break; break;
} }
@@ -19206,7 +19211,8 @@ class json_pointer
JSON_THROW(detail::out_of_range::create(402, detail::concat("array index '-' (", std::to_string(ptr->m_data.m_value.array->size()), ") is out of range"), ptr)); JSON_THROW(detail::out_of_range::create(402, detail::concat("array index '-' (", std::to_string(ptr->m_data.m_value.array->size()), ") is out of range"), ptr));
} }
// use unchecked array access // use unchecked array access; the const operator[]
// asserts that the index exists
ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token)); ptr = &ptr->operator[](array_index<BasicJsonType>(reference_token));
break; break;
} }
@@ -28749,6 +28755,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
// const operator[] only works for arrays // const operator[] only works for arrays
if (JSON_HEDLEY_LIKELY(is_array())) if (JSON_HEDLEY_LIKELY(is_array()))
{ {
JSON_ASSERT(idx < m_data.m_value.array->size());
return m_data.m_value.array->operator[](idx); return m_data.m_value.array->operator[](idx);
} }
-54
View File
@@ -241,52 +241,6 @@ class my_allocator : public std::allocator<T>
}; };
}; };
/////////////////////////////////////////////////////////////////////
// for #3669
/////////////////////////////////////////////////////////////////////
// mimics boost::optional's converting constructor, whose SFINAE check asks
// whether T is constructible from const U&
template<class T, class Arg>
struct issue3669_is_constructible
{
template<class T2, class A2, class = decltype(T2(std::declval<A2>()))>
static char test(int);
template<class, class>
static long test(...);
static constexpr bool value = sizeof(test<T, Arg>(0)) == 1;
};
template<class T>
class issue3669_optional
{
public:
issue3669_optional() = default;
template<class U>
issue3669_optional(const issue3669_optional<U>& /*unused*/, // NOLINT(google-explicit-constructor,hicpp-explicit-conversions)
typename std::enable_if<issue3669_is_constructible<T, const U&>::value, bool>::type /*unused*/ = true) {}
};
class Issue3669Dummy
{
public:
explicit Issue3669Dummy(const json& /*unused*/) {}
};
class Issue3669Holder
{
issue3669_optional<Issue3669Dummy> d{};
// GCC < 11 (C++11/14) rejects a free to_json(json&, const Issue3669Holder&)
// here, because ADL for Issue3669Dummy finds it and closes an instantiation
// cycle; a hidden friend is only visible to ADL for Issue3669Holder
friend void to_json(json& j, const Issue3669Holder& h)
{
static_cast<void>(h.d); // silence -Wunused-private-field
j = "holder";
}
};
TEST_CASE("regression tests 2") TEST_CASE("regression tests 2")
{ {
SECTION("issue #1001 - Fix memory leak during parser callback") SECTION("issue #1001 - Fix memory leak during parser callback")
@@ -812,14 +766,6 @@ TEST_CASE("regression tests 2")
CHECK(j == k); CHECK(j == k);
} }
SECTION("issue #3669 - invalid use of incomplete type with optional member and to_json")
{
const Issue3669Holder h{};
const Issue3669Holder h2(h); // NOLINT(performance-unnecessary-copy-initialization)
const json j = h2;
CHECK(j == "holder");
}
} }
TEST_CASE("regression test - parser callback must not lose a duplicate key's prior value") TEST_CASE("regression test - parser callback must not lose a duplicate key's prior value")