Docs: erase() of an empty range on a primitive throws, stale assertion messages, moved elements and the strong guarantee of object()
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 78/100
- Tipo di issue
- Documentazione
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- cpp
- Ambito
- documentation
Direzione di ricerca
Inizia dalle tre pagine indicate nella issue: api/basic_json/erase.md (il punto 1 e l'elenco puntato sul 204), features/assertions.md e api/basic_json/basic_json.md, insieme a api/basic_json/object.md per la frase sulla garanzia forte. Esegui i due programmi riproduttori per confermare il comportamento attuale, poi sostituisci il testo con i blocchi corretti forniti nella issue. È completato quando ogni pagina corrisponde a ciò che stampano i riproduttori. Non modificare il codice della libreria.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Description
Three small documentation errors, found while checking the pages against develop. Each item has the corrected text.
1. erase(first, last): "erasing an empty range is a no-op" is not true for primitive values
api/basic_json/erase.md, overload (2), says: "The iterator first does not need to be dereferenceable if first == last: erasing an empty range is a no-op."
That holds for arrays and objects. For a boolean, number, string or binary value, erase(begin(), begin()) and erase(end(), end()) throw invalid_iterator.204; for null, every range throws type_error.307. The exceptions list of the same page also states the 204 condition with "and" where the code uses "or": "if first != begin() and last != end()".
The code: include/nlohmann/json.hpp#L4257-L4260 and #L3087-L3090
if (JSON_HEDLEY_UNLIKELY(!primitive_range_is_complete(first, last)))
{
detail::throw_literal<invalid_iterator>(detail::exception_id::iterator_range_out_of_range, "iterators out of range", this);
}
...
static bool primitive_range_is_complete(const IteratorType& first, const IteratorType& last) noexcept
{
return first.m_it.primitive_iterator.is_begin() && last.m_it.primitive_iterator.is_end();
}
Is the code or the page wrong? The page. The throwing is deliberate and consistent elsewhere:
invalid_iterator.204says the range for a primitive value "has to be exactly (begin(),end()),[...] All other ranges are invalid."- The range constructor
basic_json(first, last)uses the sameprimitive_range_is_completerule (include/nlohmann/json.hppline 2575). tests/src/unit-element_access1.cpp(lines 831-893, sectionerase with two invalid iterators) asserts 204 forerase(j.end(), j.end())anderase(j.begin(), j.begin())on strings, numbers, booleans and binary values, foriteratorandconst_iterator.
There is an argument for changing the code later: a primitive iterates like a one-element container, and for such a container [begin, begin) and [end, end) are valid empty ranges. The erase-remove idiom j.erase(std::remove(j.begin(), j.end(), x), j.end()) therefore throws 204 on a number that does not equal x (see the reproducer), although it works on arrays. Making empty ranges a no-op would turn an exception into success, change the 20 assertions at lines 831-893 of unit-element_access1.cpp, and should also decide what null does (it throws 307 today, even though begin() == end()). That is a separate enhancement; this issue only corrects the page.
Corrected text for erase.md, item 2 of the description (lines 29-32):
2. Remove an element range specified by `[first; last)` from a JSON value. For arrays and objects, the iterator `first`
does not need to be dereferenceable if `first == last`: erasing an empty range is a no-op.
If called on a primitive type other than `#!json null`, the range must be `[begin(); end())`, and the resulting JSON
value will be `#!json null`. Any other range, including the empty ranges `[begin(); begin())` and `[end(); end())`,
throws [`invalid_iterator.204`](../../home/exceptions.md#jsonexceptioninvalid_iterator204).
Corrected text for erase.md, the 204 bullet of exception item 2 (lines 95-97):
- Throws [`invalid_iterator.204`](../../home/exceptions.md#jsonexceptioninvalid_iterator204) if called on a
primitive type with invalid iterators (i.e., if `first != begin()` or `last != end()`, which includes the empty
ranges `[begin(); begin())` and `[end(); end())`); example: `"iterators out of range"`
2. features/assertions.md: the recorded assertion messages no longer match
The four "Output" blocks of Runtime Assertions were recorded with older versions. Two show the wrong function or the message of a different assertion, and all line numbers have moved:
| Example | Page shows | develop prints |
|---|---|---|
| missing object key (line 52) | function operator[], file json.hpp, line 28795 |
same assertion, line 43219 |
| invalid array index in a JSON pointer (line 75) | (idx < m_data.m_value.array->size()), function operator[] |
(idx < ptr->m_data.m_value.array->size()), function get_unchecked |
| uninitialized iterator range (line 102) | (m_object != nullptr), function operator++, file iter_impl.hpp, line 368 (the message of the next example) |
(first.m_object != nullptr), function basic_json |
| uninitialized iterator (line 129) | file iter_impl.hpp, line 368 |
file json.hpp, line 25824 |
Recorded on develop 5bad6c518b with each example copied from the page, compiled against single_include/nlohmann/json.hpp (clang++ -std=c++11 -O0, Apple clang 21, macOS arm64). Corrected output blocks:
Line 52:
Assertion failed: (it != m_data.m_value.object->end()), function operator[], file json.hpp, line 43219.
Line 75:
Assertion failed: (idx < ptr->m_data.m_value.array->size()), function get_unchecked, file json.hpp, line 27906.
Line 102:
Assertion failed: (first.m_object != nullptr), function basic_json, file json.hpp, line 41848.
Line 129:
Assertion failed: (m_object != nullptr), function operator++, file json.hpp, line 25824.
The fifth example on the page (reading from a null FILE*) still prints the documented parse_error.101 message.
The same first example appears in api/macros/json_assert.md (line 53) with an even older message, (m_value.object->find(key) != m_value.object->end()), function operator[], file json.hpp, line 2144.; it should get the line-52 text above.
The line numbers change with every edit of json.hpp. To keep the pages correct longer, the outputs could end at the function name and say that file and line depend on the version, for example Assertion failed: (it != m_data.m_value.object->end()), function operator[], file json.hpp, line .... The text above is the exact output if you prefer to keep real numbers.
3. basic_json.md / object.md: the strong guarantee does not cover elements moved into the initializer list
api/basic_json/basic_json.md, exception safety item 5 (initializer-list constructor), and api/basic_json/object.md both say: "Strong guarantee: if an exception is thrown, there are no changes to any JSON value."
json::object({std::move(a), 5}) throws type_error.301 and leaves a as null. The move happens when the braced list is built: each element becomes a json_ref, and include/nlohmann/detail/json_ref.hpp#L27-L29 moves an rvalue into its own storage:
json_ref(value_type&& value)
: owned_value(std::move(value))
{}
So a is changed before object() or the constructor runs, and nothing in the library can undo that. The library code is fine; the guarantee needs a caveat. The same applies to basic_json(init, false, value_t::object). With a copy (json::object({c, 5})), c is unchanged.
Corrected text for basic_json.md, exception safety item 5 (line 268):
5. Strong guarantee: if an exception is thrown, there are no changes to any JSON value. Values that are moved into
`init` (e.g., `a` in `#!cpp json::object({std::move(a), 5})`) are moved when the initializer list is created,
before the constructor runs, and stay moved-from if it throws.
Corrected text for object.md, exception safety (line 21):
Strong guarantee: if an exception is thrown, there are no changes in the JSON value. Values that are moved into `init`
(e.g., `a` in `#!cpp json::object({std::move(a), 5})`) are moved when the initializer list is created, before this
function runs, and stay moved-from if it throws.
(array.md has the same sentence; array() only throws on allocation failure, so the caveat is optional there.)
Since when: items 1 and 3: 3.12.0 gives the same output (checked with the reproducers below). Item 2: the messages changed with later code changes; I did not check which release first differed.
Security: Not a vulnerability: documentation only.
Related: #5670 (range constructor and binary values, closed).
Possible fix: replace the text as given above. No code change. If the erase behaviour should change instead, that needs a separate issue (see the end of item 1).
Test: none needed for the documentation. Optional, to pin the erase behaviour that the page now describes (tests/src/unit-element_access1.cpp, section erase with two invalid iterators; the file then needs #include <algorithm>):
SECTION("empty range on a primitive value")
{
json j = 42;
CHECK_THROWS_WITH_AS(j.erase(std::remove(j.begin(), j.end(), json(7)), j.end()),
"[json.exception.invalid_iterator.204] iterators out of range", json::invalid_iterator&);
CHECK(j == 42);
}
Reproduction steps
- Save the first program as
erase_empty.cppand the second asobject_moved.cpp. clang++ -std=c++17 -O2 -I include erase_empty.cpp -o erase_empty && ./erase_emptyclang++ -std=c++17 -O2 -I include object_moved.cpp -o object_moved && ./object_moved- For item 2, copy each example from
features/assertions.mdinto a file and build it withoutNDEBUG:clang++ -std=c++11 -O0 -I single_include ex.cpp -o ex && ./ex.
Expected vs. actual results
- Expected: the pages describe what the library does.
- Actual: an empty range on a primitive throws 204 instead of being a no-op; the assertion outputs differ as in the table; a value moved into a failing
json::objectcall is changed although the page promises no changes.
Minimal code example
// erase_empty.cpp
#include <algorithm>
#include <iostream>
#include <nlohmann/json.hpp>
using nlohmann::json;
template<typename F>
void run(const char* label, F f)
{
try { f(); std::cout << label << ": no exception\n"; }
catch (const json::exception& e) { std::cout << label << ": " << e.what() << '\n'; }
}
int main()
{
json arr = {1, 2, 3};
run("array, erase(begin, begin)", [&] { arr.erase(arr.begin(), arr.begin()); });
json obj = {{"a", 1}};
run("object, erase(end, end) ", [&] { obj.erase(obj.end(), obj.end()); });
json num = 42;
run("number, erase(begin, begin)", [&] { num.erase(num.begin(), num.begin()); });
run("number, erase(end, end) ", [&] { num.erase(num.end(), num.end()); });
json str = "abc";
run("string, erase(end, end) ", [&] { str.erase(str.end(), str.end()); });
json nul = nullptr;
run("null, erase(begin, end) ", [&] { nul.erase(nul.begin(), nul.end()); });
// erase-remove idiom that removes nothing
json val = 42;
run("number, erase-remove idiom ", [&] { val.erase(std::remove(val.begin(), val.end(), json(7)), val.end()); });
std::cout << "num=" << num << " val=" << val << '\n';
run("number, erase(begin, end) ", [&] { num.erase(num.begin(), num.end()); });
std::cout << "num=" << num << '\n';
}
// object_moved.cpp
#include <iostream>
#include <nlohmann/json.hpp>
using nlohmann::json;
int main()
{
json a = {"key", "value"};
try { json j = json::object({std::move(a), 5}); }
catch (const json::exception& e) { std::cout << e.what() << '\n'; }
std::cout << "a after json::object: " << a << '\n';
json b = {"key", "value"};
try { json j(json::initializer_list_t{std::move(b), 5}, false, json::value_t::object); }
catch (const json::exception& e) { std::cout << e.what() << '\n'; }
std::cout << "b after basic_json(init, false, object): " << b << '\n';
// with a copy, the strong guarantee holds
json c = {"key", "value"};
try { json j = json::object({c, 5}); }
catch (const json::exception& e) { std::cout << e.what() << '\n'; }
std::cout << "c after json::object with a copy: " << c << '\n';
}
Error messages
erase_empty (develop and 3.12.0 print the same):
array, erase(begin, begin): no exception
object, erase(end, end) : no exception
number, erase(begin, begin): [json.exception.invalid_iterator.204] iterators out of range
number, erase(end, end) : [json.exception.invalid_iterator.204] iterators out of range
string, erase(end, end) : [json.exception.invalid_iterator.204] iterators out of range
null, erase(begin, end) : [json.exception.type_error.307] cannot use erase() with null
number, erase-remove idiom : [json.exception.invalid_iterator.204] iterators out of range
num=42 val=42
number, erase(begin, end) : no exception
num=null
object_moved (develop and 3.12.0 print the same):
[json.exception.type_error.301] cannot create object from initializer list
a after json::object: null
[json.exception.type_error.301] cannot create object from initializer list
b after basic_json(init, false, object): null
[json.exception.type_error.301] cannot create object from initializer list
c after json::object with a copy: ["key","value"]
Assertion examples from features/assertions.md, built against single_include/ on develop:
Assertion failed: (it != m_data.m_value.object->end()), function operator[], file json.hpp, line 43219.
Assertion failed: (idx < ptr->m_data.m_value.array->size()), function get_unchecked, file json.hpp, line 27906.
Assertion failed: (first.m_object != nullptr), function basic_json, file json.hpp, line 41848.
Assertion failed: (m_object != nullptr), function operator++, file json.hpp, line 25824.
Compiler and operating system
Apple clang 21.0.0 (clang-2100.4.1.1), macOS 27.0.1, arm64
Library version
develop (5bad6c518b); 3.12.0: affected for items 1 and 3 (same output); item 2 not checked against 3.12.0
Found in a bug hunt on develop (2026-11-10); drafted with Claude Code.
- Lingua principale
- C++
- Stelle
- 50.9k
- Fork
- 7.5k
- Merge medio
- 1g 3h
- PR unite (30g)
- 217
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di nlohmann/json
-
kind: bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
I maintainer di solito rispondono entro 1 giorno
-
kind: bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
I maintainer di solito rispondono entro 1 giorno
-
kind: bug performance
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
I maintainer di solito rispondono entro 1 giorno
-
dump(): the recursive frame grew from 480 to 2304 bytes in unoptimized builds, so 128 levels need 288 KiB of stackForse già presa @nlohmann l’ha presa oggi. Apertakind: enhancement/improvement
nlohmann/json#6116 · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
CI kind: enhancement/improvement tests
Difficoltà 3/5 1-2 giorni Idoneità per principianti 45/100
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di nlohmann/json
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
bazel-contrib/rules_go#4784 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
I maintainer di solito rispondono entro 1 giorno
-
TUIs: Fix TUI CMakesApertabug
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
RMIT-Rover-Team/equinox-2#145 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
eunomia-bpf/bpftime#779 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 79/100
I maintainer di solito rispondono entro 1 giorno