diff --git a/docs/mkdocs/docs/features/json_view.md b/docs/mkdocs/docs/features/json_view.md index 09779dba2..08529fd7d 100644 --- a/docs/mkdocs/docs/features/json_view.md +++ b/docs/mkdocs/docs/features/json_view.md @@ -253,6 +253,34 @@ Two conversions never copy at all: Both results are only valid as long as the view — and, for a string with no escapes, the borrowed source text — is. +### `string_view` members + +A struct whose member is a `#!cpp std::string_view` (C++17) can be decoded from a view with the +`NLOHMANN_DEFINE_TYPE_*` macros, and the member is not copied either: for a string without escape sequences it points +into the document's source text, and for a string with escape sequences into the document's buffer of decoded strings. +The member is valid as long as the document is alive and, for a document that +[borrows its source text](#ownership-and-lifetime) (as [`parse()`](../api/basic_json_document/parse.md) does for +an lvalue `#!cpp std::string`, a `#!cpp std::string_view`, or a C string), as long as that text is. + +```cpp +struct record +{ + std::string_view name; + int count; + NLOHMANN_DEFINE_TYPE_INTRUSIVE(record, name, count) +}; + +const std::string text = R"({"name": "widget", "count": 3})"; +const json_document doc = json_document::parse(text); // borrows text +const record r = doc.root().get(); // r.name points into text, valid while text and doc are +``` + +!!! warning "Decoding into a `string_view` from a `json` value dangles at once" + + The same struct also compiles with `#!cpp json::parse(text).get()`, but there `name` points into the + temporary `json` value, which is destroyed at the end of the statement. Use a named + [`json_document`](../api/json_document.md), as above, to decode such a struct. + ## Writing generic code A view's type names its `BasicJsonType` and its [`view_mode`](../api/view_mode.md): diff --git a/tests/src/unit-json_view_from_json.cpp b/tests/src/unit-json_view_from_json.cpp index 7e84a9081..85faaf9b6 100644 --- a/tests/src/unit-json_view_from_json.cpp +++ b/tests/src/unit-json_view_from_json.cpp @@ -39,6 +39,7 @@ using nlohmann::ordered_json_document; #ifdef JSON_HAS_CPP_17 #include + #include #endif // NLOHMANN_JSON_SERIALIZE_ENUM uses a static std::pair; same() compares floating-point values on purpose @@ -469,5 +470,70 @@ TEST_CASE("json_view from_json") #endif } +#ifdef JSON_HAS_CPP_17 +namespace view_from_json +{ +// a string_view member is not copied: it points into the source text or, for a +// string with escape sequences, into the buffer of decoded strings of the document +struct named_view +{ + // initialized for GCC -Weffc++ + // NOLINTNEXTLINE(readability-redundant-member-init) + std::string_view name{}; + int n = 0; + NLOHMANN_DEFINE_TYPE_INTRUSIVE(named_view, name, n) +}; +} // namespace view_from_json + +TEST_CASE("json_view from_json: string_view members borrow from the document") +{ + using view_from_json::named_view; + + // whether all of s is inside the bytes of text + const auto within = [](std::string_view s, std::string_view text) + { + const auto first = reinterpret_cast(text.data()); + const auto begin = reinterpret_cast(s.data()); + return begin >= first && begin + s.size() <= first + text.size(); + }; + + SECTION("a string without escapes points into the source text") + { + const std::string text = R"({"name": "hello", "n": 1})"; + const json_document d = json_document::parse(text); + REQUIRE_FALSE(d.owns_source()); + const named_view r = d.root().get(); + CHECK(r.name == "hello"); + CHECK(r.n == 1); + CHECK(within(r.name, text)); + CHECK(within(r.name, d.source())); + } + + SECTION("a string with escapes points into the document, not into the source text") + { + const std::string text = R"({"name": "a\nb", "n": 2})"; + json_document d = json_document::parse(text); + REQUIRE_FALSE(d.owns_source()); + const named_view r = d.root().get(); + CHECK(r.name == "a\nb"); + CHECK(r.n == 2); + CHECK_FALSE(within(r.name, text)); + // valid as long as the document is alive, even if the document is moved + const json_document moved = std::move(d); + CHECK(r.name == "a\nb"); + CHECK(moved.root().get().name == r.name); + } + + SECTION("a document that owns its text keeps the members readable while it is alive") + { + const json_document d = json_document::parse(std::string(R"({"name": "owned", "n": 3})")); + REQUIRE(d.owns_source()); + const named_view r = d.root().get(); + CHECK(r.name == "owned"); + CHECK(within(r.name, d.source())); + } +} +#endif + DOCTEST_CLANG_SUPPRESS_WARNING_POP DOCTEST_GCC_SUPPRESS_WARNING_POP