diff --git a/docs/mkdocs/docs/api/basic_json_document/root.md b/docs/mkdocs/docs/api/basic_json_document/root.md
index 85682dbcd..3ccedeb4a 100644
--- a/docs/mkdocs/docs/api/basic_json_document/root.md
+++ b/docs/mkdocs/docs/api/basic_json_document/root.md
@@ -1,10 +1,15 @@
# nlohmann::basic_json_document::root
```cpp
-view_type root() const noexcept;
+// (1)
+view_type root() const& noexcept;
+
+// (2)
+view_type root() const&& = delete;
```
-Returns a view of the root value of the document.
+1. Returns a view of the root value of the document.
+2. Deleted: the view of a temporary document would dangle.
## Return value
@@ -21,6 +26,16 @@ Constant.
## Notes
+**Lifetime.** A view refers into the document, so the document must outlive it. `root()` can therefore only be called
+on a document that has a name (an lvalue); calling it on a temporary does not compile:
+
+```cpp
+auto v = json_document::parse(text).root(); // error: the document is destroyed at the end of the statement
+
+auto doc = json_document::parse(text); // OK: keep the document alive
+auto v = doc.root();
+```
+
`root()` is a cheap handle into the document's index, not a copy of anything; call it as often as needed. The
returned view is valid under the same conditions as any other view of the document -- see
[Object inspection](../basic_json_view/index.md) -- in particular, it is invalidated by the next
diff --git a/docs/mkdocs/docs/features/json_view.md b/docs/mkdocs/docs/features/json_view.md
index 6b66c42ca..c6280ef62 100644
--- a/docs/mkdocs/docs/features/json_view.md
+++ b/docs/mkdocs/docs/features/json_view.md
@@ -76,6 +76,9 @@ Moving the document itself is fine and does **not** invalidate its views: the in
that keeps its address across the move. Take a fresh view from [`root()`](../api/basic_json_document/root.md)
whenever any of the other conditions above was not met.
+Because a view dies with its document, [`root()`](../api/basic_json_document/root.md) is not callable on a temporary
+document: `#!cpp auto v = json_document::parse(text).root();` does not compile. Give the document a name first.
+
??? example "Example: borrowed and owned documents, and when views become invalid"
```cpp
diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp
index 66a48960e..e2cb57c76 100644
--- a/include/nlohmann/json_view.hpp
+++ b/include/nlohmann/json_view.hpp
@@ -358,7 +358,7 @@ class basic_json_document
////////////
/// the root value (discarded if parsing failed without exceptions)
- view_type root() const noexcept
+ view_type root() const& noexcept
{
if (!m_data || m_data->discarded)
{
@@ -367,6 +367,9 @@ class basic_json_document
return view_type(m_data.get(), m_data->tape);
}
+ /// deleted: the view of a temporary document would dangle
+ view_type root() const&& = delete;
+
bool is_discarded() const noexcept
{
return !m_data || m_data->discarded;
diff --git a/tests/src/unit-json_view.cpp b/tests/src/unit-json_view.cpp
index 4e598c617..ffd44ca17 100644
--- a/tests/src/unit-json_view.cpp
+++ b/tests/src/unit-json_view.cpp
@@ -32,6 +32,22 @@ using nlohmann::ordered_json_document;
namespace
{
+// the value of a text, through a named document: the views of a temporary
+// document would dangle (root() of an rvalue document does not compile)
+template
+auto materialized(Args&& ... args) -> decltype(std::declval().materialize())
+{
+ const Document d = Document::parse(std::forward(args)...);
+ return d.root().materialize();
+}
+
+template
+auto materialized_copy(Input&& input) -> decltype(std::declval().materialize())
+{
+ const Document d = Document::parse_copy(std::forward(input));
+ return d.root().materialize();
+}
+
// detection of calls that must not compile
template
using parse_call_t = decltype(json_document::parse(std::declval()...));
@@ -41,6 +57,8 @@ template
using accept_call_t = decltype(json_document::accept(std::declval()...));
template
using read_call_t = decltype(std::declval().read(std::declval()...));
+template
+using root_call_t = decltype(std::declval().root());
template
using bool_conversion_t = decltype(static_cast(std::declval()));
@@ -179,19 +197,19 @@ TEST_CASE("json_view")
std::string text;
g.value(text, 0);
CAPTURE(text)
- CHECK(json_document::parse(text).root().materialize() == json::parse(text));
+ CHECK(materialized(text) == json::parse(text));
// member order as ordered_json::parse keeps it
- CHECK(ordered_json_document::parse(text).root().materialize().dump() == ordered_json::parse(text).dump());
+ CHECK(materialized(text).dump() == ordered_json::parse(text).dump());
}
// duplicate keys: the last value, at the position of the first key
- CHECK(json_document::parse(R"({"a":1,"b":2,"a":3})").root().materialize() == json::parse(R"({"a":1,"b":2,"a":3})"));
- CHECK(ordered_json_document::parse(R"({"a":1,"b":2,"a":3})").root().materialize().dump() == R"({"a":3,"b":2})");
+ CHECK(materialized(R"({"a":1,"b":2,"a":3})") == json::parse(R"({"a":1,"b":2,"a":3})"));
+ CHECK(materialized(R"({"a":1,"b":2,"a":3})").dump() == R"({"a":3,"b":2})");
// very deep nesting (iterative, as parse())
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
- CHECK(json_document::parse(deep).root().materialize() == json::parse(deep));
+ CHECK(materialized(deep) == json::parse(deep));
#if JSON_DIAGNOSTICS
// the parents are set, so errors name the path
- const json m = json_document::parse(R"({"a":{"b":[1]}})").root().materialize();
+ const json m = materialized(R"({"a":{"b":[1]}})");
CHECK_THROWS_WITH_AS(m.at("a").at("b").at(0).at("x"), "[json.exception.type_error.304] (/a/b/0) cannot use at() with number", json::type_error&);
#endif
}
@@ -264,7 +282,7 @@ TEST_CASE("json_view")
CHECK(json_document::accept(text));
if (accepted)
{
- CHECK(float_document::parse(text).root().materialize() == json_float::parse(text));
+ CHECK(materialized(text) == json_float::parse(text));
}
}
float_document f;
@@ -278,7 +296,7 @@ TEST_CASE("json_view")
CHECK(json_document::accept(with_nul) == json::accept(with_nul));
const std::string nul_in_comment("[1, // c\0\n2]", 12);
CHECK(json_document::accept(nul_in_comment, true) == json::accept(nul_in_comment, true));
- CHECK(json_document::parse("\xEF\xBB\xBF[1]").root().materialize() == json::parse("\xEF\xBB\xBF[1]"));
+ CHECK(materialized("\xEF\xBB\xBF[1]") == json::parse("\xEF\xBB\xBF[1]"));
#if !defined(JSON_NOEXCEPTION)
CHECK(view_exception("\xEF\xBB") == parse_exception("\xEF\xBB"));
#endif
@@ -294,18 +312,18 @@ TEST_CASE("json_view")
CHECK(!borrowed.owns_source());
CHECK(borrowed.source().data() == text.data());
CHECK(borrowed.root().materialize() == expected);
- CHECK(json_document::parse(text.c_str()).root().materialize() == expected);
- CHECK(json_document::parse(R"([1, "two", {"three": 3.5}])").root().materialize() == expected);
- CHECK(json_document::parse(text.data(), text.data() + text.size()).root().materialize() == expected);
+ CHECK(materialized(text.c_str()) == expected);
+ CHECK(materialized(R"([1, "two", {"three": 3.5}])") == expected);
+ CHECK(materialized(text.data(), text.data() + text.size()) == expected);
const std::vector chars(text.begin(), text.end());
CHECK(!json_document::parse(chars).owns_source());
- CHECK(json_document::parse(chars).root().materialize() == expected);
+ CHECK(materialized(chars) == expected);
const std::vector bytes(text.begin(), text.end());
- CHECK(json_document::parse(bytes).root().materialize() == expected);
+ CHECK(materialized(bytes) == expected);
#ifdef JSON_HAS_CPP_17
const std::string_view sv = text;
CHECK(!json_document::parse(sv).owns_source());
- CHECK(json_document::parse(sv).root().materialize() == expected);
+ CHECK(materialized(sv) == expected);
#endif
// owned
@@ -328,14 +346,14 @@ TEST_CASE("json_view")
const std::vector const_chars(text.begin(), text.end());
CHECK(json_document::parse(std::move(const_chars)).owns_source()); // NOLINT(performance-move-const-arg,hicpp-move-const-arg)
CHECK(json_document::parse_copy(text).owns_source());
- CHECK(json_document::parse_copy(text).root().materialize() == expected);
+ CHECK(materialized_copy(text) == expected);
std::istringstream stream(text);
const json_document from_stream = json_document::parse(stream);
CHECK(from_stream.owns_source());
CHECK(from_stream.root().materialize() == expected);
const std::list list(text.begin(), text.end());
CHECK(json_document::parse(list.begin(), list.end()).owns_source());
- CHECK(json_document::parse(list.begin(), list.end()).root().materialize() == expected);
+ CHECK(materialized(list.begin(), list.end()) == expected);
// iterator pairs: pointers are borrowed, and so are contiguous library
// iterators where the input adapter detects them (C++20)
@@ -346,10 +364,10 @@ TEST_CASE("json_view")
CHECK((from_iterators.source().data() == chars.data()) == contiguous);
CHECK(from_iterators.root().materialize() == expected);
const std::string padded = "x" + text + "x";
- CHECK(json_document::parse(padded.begin() + 1, padded.end() - 1).root().materialize() == expected);
+ CHECK(materialized(padded.begin() + 1, padded.end() - 1) == expected);
CHECK(json_document::parse(chars.cbegin(), chars.cbegin(), false).is_discarded());
const std::wstring wide = L"[\"\u00e4\u20ac\", 1]";
- CHECK(json_document::parse(wide).root().materialize() == json::parse(wide));
+ CHECK(materialized(wide) == json::parse(wide));
CHECK(json_document::parse(static_cast(nullptr), false).is_discarded());
CHECK(json_document::parse("", false).is_discarded());
}
@@ -386,10 +404,29 @@ TEST_CASE("json_view")
// the valid calls still work
const char* const text = "[1]";
- CHECK(json_document::parse(text, true).root().is_array());
+ CHECK(materialized(text, true) == json::parse(text));
CHECK(json_document::accept(text, true, true));
}
+ SECTION("root of a temporary document does not compile")
+ {
+ using nlohmann::detail::is_detected;
+
+ // the view would dangle: auto v = json_document::parse(text).root();
+ static_assert(is_detected::value, "root() of an lvalue is valid");
+ static_assert(is_detected::value, "root() of a const lvalue is valid");
+ static_assert(!is_detected::value, "root() of an rvalue must not compile");
+ static_assert(!is_detected < root_call_t, json_document && >::value, "root() of an rvalue must not compile");
+ static_assert(!is_detected < root_call_t, const json_document && >::value, "root() of a const rvalue must not compile");
+ static_assert(!is_detected::value, "root() of an rvalue must not compile");
+
+ // a named document is fine, also after a move
+ json_document d = json_document::parse("[1]");
+ CHECK(d.root().size() == 1);
+ const json_document moved = std::move(d);
+ CHECK(moved.root().size() == 1);
+ }
+
SECTION("document lifetime and reuse")
{
json_document d;