Files
json/docs/mkdocs/docs/features/json_view/lifetime.md
T
Niels Lohmann 94331d1050 Document that read() must not be given a view of the document itself (#6086)
* Document that read() must not be given a view of the document itself

basic_json_document::read() releases the previous text, the decoded
strings and the edits before it parses its input. An input that points
into them (source(), get_string(), number_token(), or a string of an
edited document) is read after its memory was cleared or freed.

A guard that copies such input costs about 1 ns per read() (5 % of a
read of a 7-byte input), which this API exists to avoid, so state the
precondition in read.md, lifetime.md and the pages of the views that
hand out such strings instead, and show how to copy the string first.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Assert in debug builds that read() is not given its own text

read() releases the document's text, decoded strings, owned image and
edits before it parses, so an input that points into them is read after
release. Without NDEBUG, read() now asserts that a borrowed input, a
copied contiguous input and (before C++17) a string_ref input are not
held by the document: its owned text, its decoded strings, an owned
image, and the current buffer of edited strings. Release builds keep
the documented precondition without a check, which would cost 2-4.5 %
per read() of a small message.

JSON_ASSERT is undefined at the end of json.hpp, so the view uses
assert() through a new internal NLOHMANN_VIEW_ASSERT.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-11-11 22:36:17 +01:00

4.7 KiB

Ownership and lifetime

A document either borrows the text it was parsed from, or owns its own copy of it; call owns_source() to find out which happened. parse() decides this from the value category and type of its argument (an lvalue #!cpp std::string is borrowed; an rvalue #!cpp std::string is moved in, owned without a copy; a stream is read into an owned buffer; and so on — see parse's Notes for the full table). parse_copy() always owns a copy, regardless of the input.

!!! warning "A borrowed document depends on your buffer"

If a document borrows its text, that text **must outlive the document** (and every view taken from it) and must
stay **unmodified** for as long as the document exists, since floating-point values and unescaped strings are
read from it lazily. Reading or writing through a view after the underlying buffer is gone is undefined behavior,
exactly as it would be for a dangling `#!cpp std::string_view`.

When a view is valid

A view is valid only while all of the following hold:

  • the document is alive,
  • the document has not been re-parsed since the view was taken (with read() or parse() into it),
  • the document has not had shrink_to_fit() called on it since, and
  • if the document borrows its source text, that text is still alive and unmodified.

Moving the basic_json_document<> itself is fine and does not invalidate its views: the index is a separate heap allocation that keeps its address across the move. Take a fresh view from root() whenever any of the other conditions above was not met. Edits of an editable document keep views valid, too; see What stays valid.

--8<-- "images/json_view_lifetime.svg"

Four timelines of a document and its views, each bar showing when an object is alive or valid and when it dangles. Destroying the borrowed text, re-parsing, or shrinking the document invalidates the views taken before; moving the document does not.

Why views can dangle

A view can dangle, exactly like a #!cpp std::string_view: nothing stops you from using it after its document is gone. This is deliberate. A view is two pointers and trivially copyable; making it safe (a #!cpp std::shared_ptr in the document and a #!cpp std::weak_ptr in each view) would make it larger, add reference counting to every copy, and a lock() to every access — exactly the costs the view exists to avoid. What is caught is the obvious case: because a view dies with its document, root() is not callable on a temporary document: #!cpp auto v = json_document::parse(text).root(); does not compile. Give the document a name first.

The same holds for what a view hands out without copying: a string from get_string(), a token from number_token(), and string_view members of decoded structs are valid only as long as the document and, if it borrows, its source text.

This includes read(): it releases the document's text, decoded strings, and edits before it parses its input, so the input must not be a string_view obtained from the document itself: its source(), a string from get_string(), a token from number_token(), or a key from an iterator (those of a borrowed text that the caller owns, read unchanged, are not affected). To re-parse a string that the document holds, for example JSON embedded in a string value, copy the string into a #!cpp std::string first and pass the copy; see read() for the exact list.

??? example "Example: borrowed and owned documents, and when views become invalid"

```cpp
--8<-- "examples/json_view_ownership.cpp"
```

Output:

```json
--8<-- "examples/json_view_ownership.output"
```

See also

Version history

  • Added in version 3.13.0.