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;