Compare commits

..
Author SHA1 Message Date
Niels Lohmann a6ab196ad5 Write integers of 17 to 20 digits eight digits at a time in dump()
#5634 made dump() write integers below 10^16 eight digits at a time; larger
ones still took the two-digits-per-division loop and count_digits(). Random
64-bit integers almost all have 17 to 20 digits (99.9 % of signed_ints.json
and unsigned_ints.json), so Dump/signed_ints and Dump/unsigned_ints gained
only 1.4x over 3.12.0, against 2.3x for small integers.

A 64-bit magnitude is now written as its first one to four digits and two
groups of eight. Wider magnitudes (__int128 and class types, #5805) keep the
loop: number types of at most 64 bits are selected by type, so no comparison
is reported as always true; for others the value is compared. The sign
handling of #1447/#5815 (remove_sign() into number_magnitude_t) is unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-30 15:08:15 +01:00
56 changed files with 1529 additions and 4900 deletions

No files matched your search

+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
+2 -2
View File
@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
@@ -46,7 +46,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
timeout-minutes: 30
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
+4 -4
View File
@@ -27,7 +27,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
@@ -38,14 +38,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
uses: github/codeql-action/init@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
with:
languages: c-cpp
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
uses: github/codeql-action/autobuild@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
uses: github/codeql-action/analyze@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
@@ -20,7 +20,7 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
+2 -2
View File
@@ -31,7 +31,7 @@ jobs:
security-events: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
@@ -48,6 +48,6 @@ jobs:
output: 'flawfinder_results.sarif'
- name: Upload analysis results to GitHub Security tab
uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
uses: github/codeql-action/upload-sarif@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
with:
sarif_file: ${{github.workspace}}/flawfinder_results.sarif
+1 -1
View File
@@ -54,7 +54,7 @@ jobs:
timeout-minutes: 60
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
+2 -2
View File
@@ -40,7 +40,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
@@ -80,6 +80,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
uses: github/codeql-action/upload-sarif@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
with:
sarif_file: results.sarif
+2 -2
View File
@@ -36,7 +36,7 @@ jobs:
runs-on: ubuntu-26.04
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
@@ -65,7 +65,7 @@ jobs:
# Upload SARIF file generated in previous step
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
uses: github/codeql-action/upload-sarif@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
with:
sarif_file: semgrep.sarif
if: always()
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
+5 -5
View File
@@ -80,7 +80,7 @@ jobs:
runs-on: ubuntu-26.04
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
@@ -112,7 +112,7 @@ jobs:
target: [ci_test_single_header, ci_cppcheck, ci_cpplint, ci_reproducible_tests, ci_non_git_tests, ci_offline_testdata, ci_reuse_compliance, ci_test_valgrind]
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
@@ -178,7 +178,7 @@ jobs:
runs-on: ubuntu-26.04
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
@@ -433,7 +433,7 @@ jobs:
runs-on: ubuntu-26.04
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
@@ -456,7 +456,7 @@ jobs:
target: [ci_test_examples, ci_test_build_documentation, ci_test_documentation_mermaid]
steps:
- name: Harden Runner
uses: step-security/harden-runner@dc2769abcc0fcfbadfc1df54081c1608579ef6b2 # v2.22.2
uses: step-security/harden-runner@ccd8616d44fd3846e67624a50d5aad6d37bf2d25 # v2.22.1
with:
egress-policy: audit
+2 -2
View File
@@ -28,7 +28,7 @@ jobs:
with:
persist-credentials: false
- name: Set up MinGW
uses: egor-tensin/setup-mingw@d54f5ef0ce585bd3fbb1f8a71b5b254f59dbf931 # v3.0
uses: egor-tensin/setup-mingw@41b837e47d7f85214629d255b9c4bc3fcbe9fd63 # v3.0
with:
platform: ${{ matrix.architecture }}
version: 12.2.0 # https://github.com/egor-tensin/setup-mingw/issues/14
@@ -153,7 +153,7 @@ jobs:
- name: Install Clang
run: curl -fsSL -o LLVM${{ matrix.version }}.exe https://github.com/llvm/llvm-project/releases/download/llvmorg-${{ matrix.version }}/LLVM-${{ matrix.version }}-win64.exe ; 7z x LLVM${{ matrix.version }}.exe -y -o"C:/Program Files/LLVM"
- name: Set up MinGW
uses: egor-tensin/setup-mingw@d54f5ef0ce585bd3fbb1f8a71b5b254f59dbf931 # v3.0
uses: egor-tensin/setup-mingw@41b837e47d7f85214629d255b9c4bc3fcbe9fd63 # v3.0
with:
platform: x64
version: 12.2.0 # https://github.com/egor-tensin/setup-mingw/issues/14
+2 -2
View File
@@ -1408,9 +1408,9 @@ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR I
- The class contains a port of the shortest double-to-decimal conversion of [Żmij](https://github.com/vitaut/zmij) by Victor Zverovich, including the conversion of the digits to text by Xiang JunBo and the SIMD instruction sequence of Dougall Johnson, which is licensed under the [MIT License](https://opensource.org/licenses/MIT) (see above). Copyright &copy; 2025 [Victor Zverovich](https://github.com/vitaut)
- The class contains a copy of [Hedley](https://nemequ.github.io/hedley/) from Evan Nemerson which is licensed as [CC0-1.0](https://creativecommons.org/publicdomain/zero/1.0/).
- The class contains parts of [Google Abseil](https://github.com/abseil/abseil-cpp) which is licensed under the [Apache 2.0 License](https://opensource.org/licenses/Apache-2.0).
- The class contains an adapted version of the Eisel-Lemire algorithm, its table of powers of five, its digit comparison for long numbers, and its parsing of eight digits at a time from [fast_float](https://github.com/fastfloat/fast_float) by Daniel Lemire and contributors, which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here), the Apache 2.0 License, and the Boost Software License. Copyright &copy; 2021 The fast_float authors
- The class contains an adapted version of the Eisel-Lemire algorithm, its table of powers of five, and its digit comparison for long numbers from [fast_float](https://github.com/fastfloat/fast_float) by Daniel Lemire and contributors, which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here), the Apache 2.0 License, and the Boost Software License. Copyright &copy; 2021 The fast_float authors
- The view's parser (`<nlohmann/json_view.hpp>`) contains techniques and code adapted from [yyjson](https://github.com/ibireme/yyjson) by YaoYuan, which is licensed under the [MIT License](https://opensource.org/licenses/MIT) (see above): table-driven decoding of `\u` escapes and fixed-offset unrolled checks.
- The class's lexer and the view's parser (`<nlohmann/json_view.hpp>`) validate non-ASCII strings with the vector UTF-8 check of [simdjson](https://github.com/simdjson/simdjson) by Daniel Lemire, Geoff Langdale, John Keiser, and contributors (its "lookup4" algorithm and tables, after J. Keiser and D. Lemire, "Validating UTF-8 In Less Than One Instruction Per Byte", 2021), which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here) and the Apache 2.0 License. Copyright &copy; 2018-2025 The simdjson authors
- The view's parser (`<nlohmann/json_view.hpp>`) validates non-ASCII strings with the vector UTF-8 check of [simdjson](https://github.com/simdjson/simdjson) by Daniel Lemire, Geoff Langdale, John Keiser, and contributors (its "lookup4" algorithm and tables, after J. Keiser and D. Lemire, "Validating UTF-8 In Less Than One Instruction Per Byte", 2021), which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here) and the Apache 2.0 License. Copyright &copy; 2018-2025 The simdjson authors
<img align="right" src="https://git.fsfe.org/reuse/reuse-ci/raw/branch/master/reuse-horizontal.png" alt="REUSE Software">
+3 -4
View File
@@ -68,10 +68,9 @@ Strong guarantee: there are no changes to the view or the document it refers to.
- Throws [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) if `ptr` cannot be resolved
because a reference token is used on a primitive value.
As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) these exceptions name the JSON
Pointer path of the value within the document, and with
[`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) its bytes in the source text (when
[`materialize()`](materialize.md) sets them).
None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no
`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with
`JSON_DIAGNOSTICS` enabled.
## Complexity
+3 -3
View File
@@ -20,9 +20,9 @@ Strong guarantee: there are no changes to the view or the document it refers to.
Throws [`invalid_iterator.214`](../../home/exceptions.md#jsonexceptioninvalid_iterator214) if the view is
[null](is_null.md) or [discarded](is_discarded.md), or if it is an empty array or object.
As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the exception names the JSON Pointer
path of the value within the document, and with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md)
its bytes in the source text (when [`materialize()`](materialize.md) sets them).
This exception does not carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no
`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with
`JSON_DIAGNOSTICS` enabled.
## Complexity
@@ -20,9 +20,9 @@ Strong guarantee: there are no changes to the view or the document it refers to.
Throws [`invalid_iterator.214`](../../home/exceptions.md#jsonexceptioninvalid_iterator214) if the view is
[null](is_null.md) or [discarded](is_discarded.md), or if it is an empty array or object.
As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the exception names the JSON Pointer
path of the value within the document, and with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md)
its bytes in the source text (when [`materialize()`](materialize.md) sets them).
This exception does not carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no
`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with
`JSON_DIAGNOSTICS` enabled.
## Complexity
+8 -7
View File
@@ -80,13 +80,14 @@ Strong guarantee: there are no changes to the view or the document it refers to.
- For every other `T`: whatever [`materialize().get<T>()`](../basic_json/get.md) throws — typically `type_error.302`,
or whatever a user-provided `from_json()` throws.
The exceptions thrown directly by this function or by a `from_json()` that reads the view (the first four bullets
above) carry the context of those of `BasicJsonType`: with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the
JSON Pointer path of the value within the document, and with
[`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) its bytes in the source text. An exception
thrown while converting through `materialize()` (the last bullet) is thrown by the materialized `BasicJsonType` value
instead: its path is relative to the root of the view's subtree, because `materialize()` builds only the subtree
(with parent pointers), so it is empty if the failing value is the subtree's root itself.
None of the exceptions thrown directly by this function or by a `from_json()` that reads the view (the first four
bullets above) carry a
[`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no `BasicJsonType` value to point at. An
exception thrown while converting through `materialize()` (the last bullet) is different: it is thrown by a real
`BasicJsonType` value, so it **does** carry a `JSON_DIAGNOSTICS` path if `BasicJsonType` was built with it enabled.
That path is relative to the root of the view's subtree, because `materialize()` builds only the subtree (with parent
pointers): it is empty if the failing value is the subtree's root itself, and it never is the path within the whole
document.
## Complexity
@@ -22,9 +22,9 @@ Strong guarantee: there are no changes to the view or the document it refers to.
Throws [`type_error.302`](../../home/exceptions.md#jsonexceptiontype_error302) if the value is not a string; example:
`"type must be string, but is array"`.
As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the exception names the JSON Pointer
path of the value within the document, and with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md)
its bytes in the source text (when [`materialize()`](materialize.md) sets them).
This exception does not carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no
`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with
`JSON_DIAGNOSTICS` enabled.
## Complexity
@@ -22,9 +22,9 @@ Strong guarantee: there are no changes to the view or the document it refers to.
Throws [`type_error.302`](../../home/exceptions.md#jsonexceptiontype_error302) if the value is not a number; example:
`"type must be number, but is string"`.
As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) the exception names the JSON Pointer
path of the value within the document, and with [`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md)
its bytes in the source text (when [`materialize()`](materialize.md) sets them).
This exception does not carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no
`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with
`JSON_DIAGNOSTICS` enabled.
## Complexity
@@ -73,10 +73,9 @@ Strong guarantee: there are no changes to the view or the document it refers to.
- [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) if an array index in `ptr` is not a
number.
As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) these exceptions name the JSON
Pointer path of the value within the document, and with
[`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) its bytes in the source text (when
[`materialize()`](materialize.md) sets them).
None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no
`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with
`JSON_DIAGNOSTICS` enabled.
## Complexity
@@ -61,10 +61,9 @@ Strong guarantee: there are no changes to the view or the document it refers to.
`default_value` instead of throwing, exactly as [`BasicJsonType::value`](../basic_json/value.md) catches
`out_of_range` and returns `default_value`.
As for `BasicJsonType`, with [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) these exceptions name the JSON
Pointer path of the value within the document, and with
[`JSON_DIAGNOSTIC_POSITIONS`](../macros/json_diagnostic_positions.md) its bytes in the source text (when
[`materialize()`](materialize.md) sets them).
None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no
`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with
`JSON_DIAGNOSTICS` enabled.
## Complexity
+15 -19
View File
@@ -134,21 +134,9 @@ document: `#!cpp auto v = json_document::parse(text).root();` does not compile.
- **Macros.** [`JSON_STRICT_NUL_HANDLING`](../api/macros/json_strict_nul_handling.md) and
[`JSON_NOEXCEPTION`](../api/macros/json_noexception.md)/[`JSON_THROW_USER`](../api/macros/json_throw_user.md)
behave the same way they do for `<nlohmann/json.hpp>`. These are the macros that change what the parser accepts or
how it reports errors. Two more macros add context to the exceptions of views, as they do for those of
`#!cpp basic_json`:
- With [`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md), an exception names the JSON Pointer path of the
value within the document.
- With [`JSON_DIAGNOSTIC_POSITIONS`](../api/macros/json_diagnostic_positions.md), an exception names the bytes of
the value in the source text.
A view has neither parent pointers nor stored positions, so both are found from the document's index when the
exception is thrown. This takes time linear in the size of the document. One exception has neither:
`key()` of an iterator over an array or a primitive value
([`invalid_iterator.207`](../home/exceptions.md#jsonexceptioninvalid_iterator207)), because a view's iterator does
not know the value it iterates over.
The other macros only affect the values `materialize()` produces, through `BasicJsonType`.
how it reports errors. The others either do not apply ([`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md)
needs parent pointers in `basic_json` values) or only affect the values `materialize()` produces, through
`BasicJsonType`.
## What is different
@@ -179,6 +167,10 @@ document: `#!cpp auto v = json_document::parse(text).root();` does not compile.
[`at`](../api/basic_json_view/at.md) throws for every missing value.
- **Duplicate keys: lookups find the first member, `parse()` keeps the last.** See [Duplicate keys](#duplicate-keys)
below.
- **No [`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md) path.** Exceptions thrown by `basic_json_view`'s own
element access and lookup functions never carry the JSON Pointer path `JSON_DIAGNOSTICS` would otherwise add: the
view has no `basic_json` value to point at, so the exception is created without one, regardless of how
`BasicJsonType` was built.
- **Ordering comparisons are not provided** by `basic_json_view` — there is no `#!cpp operator<`.
[`operator==`](../api/basic_json_view/operator_eq.md) and [`operator!=`](../api/basic_json_view/operator_ne.md) are
provided, though: two views, or a view and a `BasicJsonType` value, compare equal exactly when
@@ -337,10 +329,14 @@ Its body may only use what views and `basic_json` have in common: the type check
a view does not have; such a type is converted through `materialize()`. A `from_json()` for `json` only is
converted through `materialize()` as well.
One thing differs when a `from_json()` reads the view: members looked up by name (`at()`, `value()`, ... and thus
the macros) find the **first** member with a repeated key, see [Duplicate keys](#duplicate-keys);
`#!cpp materialize().get<T>()` would see the last one. Containers built from an object, such as `#!cpp std::map`,
keep the **last** value either way, as [`parse()`](../api/basic_json/parse.md) does.
Two things differ when a `from_json()` reads the view:
- **Duplicate keys.** Members looked up by name (`at()`, `value()`, ... and thus the macros) find the **first** member
with a repeated key, see [Duplicate keys](#duplicate-keys); `#!cpp materialize().get<T>()` would see the last one.
Containers built from an object, such as `#!cpp std::map`, keep the **last** value either way, as
[`parse()`](../api/basic_json/parse.md) does.
- **[`JSON_DIAGNOSTICS`](../api/macros/json_diagnostics.md).** Exceptions thrown by the view carry no path to the
value.
`to_json()` is unaffected: it writes `basic_json` values, never views.
+2 -2
View File
@@ -22,8 +22,8 @@ The class contains a port of the shortest double-to-decimal conversion of [Żmij
The class contains a copy of [Hedley](https://nemequ.github.io/hedley/) from Evan Nemerson which is licensed as [CC0-1.0](https://creativecommons.org/publicdomain/zero/1.0/).
The class contains an adapted version of the Eisel-Lemire algorithm, its table of powers of five, its digit comparison for long numbers, and its parsing of eight digits at a time from [fast_float](https://github.com/fastfloat/fast_float) by Daniel Lemire and contributors, which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here), the Apache 2.0 License, and the Boost Software License. Copyright &copy; 2021 The fast_float authors
The class contains an adapted version of the Eisel-Lemire algorithm, its table of powers of five, and its digit comparison for long numbers from [fast_float](https://github.com/fastfloat/fast_float) by Daniel Lemire and contributors, which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here), the Apache 2.0 License, and the Boost Software License. Copyright &copy; 2021 The fast_float authors
The view's parser (`<nlohmann/json_view.hpp>`) contains techniques and code adapted from [yyjson](https://github.com/ibireme/yyjson) by YaoYuan, which is licensed under the [MIT License](https://opensource.org/licenses/MIT) (see above): table-driven decoding of `\u` escapes and fixed-offset unrolled checks.
The class's lexer and the view's parser (`<nlohmann/json_view.hpp>`) validate non-ASCII strings with the vector UTF-8 check of [simdjson](https://github.com/simdjson/simdjson) by Daniel Lemire, Geoff Langdale, John Keiser, and contributors (its "lookup4" algorithm and tables, after J. Keiser and D. Lemire, "Validating UTF-8 In Less Than One Instruction Per Byte", 2021), which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here) and the Apache 2.0 License. Copyright &copy; 2018-2025 The simdjson authors
The view's parser (`<nlohmann/json_view.hpp>`) validates non-ASCII strings with the vector UTF-8 check of [simdjson](https://github.com/simdjson/simdjson) by Daniel Lemire, Geoff Langdale, John Keiser, and contributors (its "lookup4" algorithm and tables, after J. Keiser and D. Lemire, "Validating UTF-8 In Less Than One Instruction Per Byte", 2021), which is available under the [MIT License](https://opensource.org/licenses/MIT) (used here) and the Apache 2.0 License. Copyright &copy; 2018-2025 The simdjson authors
@@ -19,11 +19,6 @@
#include <type_traits> // conditional
// The shortest-digit conversion of doubles (see zmij.hpp) and the writing of
// its digits, including the conversion of the digits to text by Xiang JunBo
// and the SIMD instruction sequence by Dougall Johnson, are ported from Zmij
// by Victor Zverovich (https://github.com/vitaut/zmij, MIT license).
//
// SSE2 (every x86-64 CPU) and NEON (every 64-bit Arm CPU) convert the 16
// digits of a double at once (unless JSON_NO_SIMD selects the portable code)
#if !defined(JSON_NO_SIMD) && (defined(__x86_64__) || (defined(_M_X64) && !defined(_M_ARM64EC)))
+1 -23
View File
@@ -431,28 +431,6 @@ std::nullptr_t exception_context(const View& /*unused*/) noexcept
return nullptr;
}
namespace view
{
// the path and bytes of the value of a view (defined in <nlohmann/detail/view/errors.hpp>)
template<typename View>
std::string diagnostics_of(const View& v);
} // namespace view
// what goes in front of the message of an exception about j: nothing for a
// basic_json value (exception_context() passes the value itself), the context
// basic_json would add (JSON_DIAGNOSTICS, JSON_DIAGNOSTIC_POSITIONS) for a view
template<typename BasicJsonType, enable_if_t<is_basic_json<BasicJsonType>::value, int> = 0>
const char* exception_prefix(const BasicJsonType& /*unused*/) noexcept
{
return "";
}
template<typename View, enable_if_t<is_json_view<View>::value, int> = 0>
std::string exception_prefix(const View& v)
{
return view::diagnostics_of(v);
}
/*!
@brief throws because @a j does not have the type a conversion expects
@param[in] expected the expected type(s), e.g. "array" or "binary or array"
@@ -464,7 +442,7 @@ JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_type_must_be(const cha
{
static_cast<void>(expected); // unused when JSON_NOEXCEPTION is defined
static_cast<void>(j);
JSON_THROW(type_error::create(exception_id::type_mismatch, concat(exception_prefix(j), "type must be ", expected, ", but is ", j.type_name()), exception_context(j)));
JSON_THROW(type_error::create(exception_id::type_mismatch, concat("type must be ", expected, ", but is ", j.type_name()), exception_context(j)));
}
/*!
@@ -44,13 +44,6 @@
// use std::from_chars or std::strtold. They are free functions so the lexer
// stays focused on scanning (see lexer::convert_number()) and so that other
// parsers of JSON text can convert tokens exactly like it does.
//
// The Eisel-Lemire conversion (eisel_lemire()), the digit comparison for the
// cases it cannot decide, and the eight-digits-at-a-time helpers
// (is_eight_digits(), parse_eight_digits()) are adapted from fast_float
// (https://github.com/fastfloat/fast_float, Daniel Lemire and contributors,
// available under the MIT, Apache 2.0, and Boost Software licenses; used here
// under the MIT license). The table of powers of five is in pow5_table.hpp.
NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail
@@ -30,13 +30,6 @@
// NLOHMANN_JSON_NO_TARGET_ATTRIBUTE (for compilers that ignore the target
// attribute and then reject the SSSE3 intrinsics) keeps the check portable
// without disabling the SSE2 code.
//
// The vector UTF-8 check (utf8_lookup4) is adapted from simdjson
// (https://github.com/simdjson/simdjson, Daniel Lemire, Geoff Langdale, John
// Keiser, and contributors, available under the Apache 2.0 and MIT licenses;
// used here under the MIT license): the "lookup4" algorithm and tables of
// J. Keiser and D. Lemire, "Validating UTF-8 In Less Than One Instruction Per
// Byte" (2021).
#if !defined(JSON_NO_SIMD) && defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) && defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
#include <arm_neon.h>
#define JSON_SCAN_NEON 1
+1 -1
View File
@@ -347,7 +347,7 @@
return ej_pair.second == j; \
}); \
if (it != std::end(m)) e = it->first; \
else ::nlohmann::detail::templated_json_throw<nlohmann::detail::out_of_range>(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range, nlohmann::detail::concat(nlohmann::detail::exception_prefix(j), "enum value out of range for " #ENUM_TYPE ": ", nlohmann::detail::from_json_dump(j)), nlohmann::detail::exception_context(j))); \
else ::nlohmann::detail::templated_json_throw<nlohmann::detail::out_of_range>(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range, nlohmann::detail::concat("enum value out of range for " #ENUM_TYPE ": ", nlohmann::detail::from_json_dump(j)), nlohmann::detail::exception_context(j))); \
}
// Ugly macros to avoid uglier copy-paste when specializing basic_json. They
@@ -1977,6 +1977,23 @@ class serializer
return;
}
// 17 to 20 digits: the first one to four, then twice eight
if (magnitude_fits_64_bits(abs_value, std::integral_constant<bool, magnitude_has_at_most_64_bits>()))
{
const auto value = static_cast<std::uint64_t>(abs_value);
const std::uint64_t top = value / 10000000000000000u; // 1 to 1844
const std::uint64_t low16 = value - (top * 10000000000000000u);
const std::uint64_t middle = low16 / 100000000u;
const std::uint64_t first = dtoa_impl::eight_digit_bytes(top);
const auto leading = static_cast<unsigned>(count_leading_zeros(first) / 8); // (first is not 0)
char* const p = buffer_ptr + n_chars;
dtoa_impl::store_msb_first(p, (first << (8 * leading)) + 0x3030303030303030u);
dtoa_impl::store_msb_first(p + 8 - leading, dtoa_impl::eight_digit_bytes(middle) + 0x3030303030303030u);
dtoa_impl::store_msb_first(p + 16 - leading, dtoa_impl::eight_digit_bytes(low16 - (middle * 100000000u)) + 0x3030303030303030u);
write_buffer_pos += n_chars + 24 - leading;
return;
}
n_chars += count_digits(abs_value);
// spare 1 byte for '\0'
@@ -2122,6 +2139,23 @@ class serializer
}
}
/// whether every number_magnitude_t fits 64 bits
static constexpr bool magnitude_has_at_most_64_bits = std::numeric_limits<number_magnitude_t>::is_specialized
&& std::numeric_limits<number_magnitude_t>::digits <= 64;
/// whether @a x fits 64 bits: always if number_magnitude_t has at most
/// 64 bits (selected by type, as the comparison would be reported as
/// always true), else compared
static bool magnitude_fits_64_bits(const number_magnitude_t& /*unused*/, std::true_type /*unused*/) noexcept
{
return true;
}
static bool magnitude_fits_64_bits(const number_magnitude_t& x, std::false_type /*unused*/) noexcept
{
return x <= 18446744073709551615u;
}
/*
* Helper function for dump_integer
*
File diff suppressed because it is too large. Load diff
+14 -51
View File
@@ -26,7 +26,6 @@
#include <nlohmann/detail/view/lookup.hpp>
#include <nlohmann/detail/view/macro_scope.hpp>
#include <nlohmann/detail/view/node.hpp>
#include <nlohmann/detail/view/scan.hpp>
#include <nlohmann/detail/view/view_mode.hpp>
NLOHMANN_JSON_NAMESPACE_BEGIN
@@ -47,9 +46,10 @@ struct first_true<false, Conditions...> : std::integral_constant < int, 1 + firs
/// (type_error.316 with the same message), so that an editable document
/// only holds valid UTF-8: the error is at the first byte that no
/// well-formed sequence can continue with (Unicode, Table 3-7).
NLOHMANN_VIEW_ALWAYS_INLINE void check_utf8_bytewise(const unsigned char* p, std::size_t n, std::size_t i)
inline void check_utf8(const char* s, std::size_t n)
{
while (i < n)
const auto* const p = reinterpret_cast<const unsigned char*>(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
for (std::size_t i = 0; i < n;)
{
const unsigned char c = p[i];
if (c < 0x80)
@@ -79,43 +79,6 @@ NLOHMANN_VIEW_ALWAYS_INLINE void check_utf8_bytewise(const unsigned char* p, std
}
}
/// The long-string path of check_utf8(): skips well-formed runs with the
/// parser's string scan (which stops at a quote, a backslash, or a control
/// character, all legal here, and at ill-formed UTF-8); the byte-wise check
/// runs only from where the scan found ill-formed UTF-8, to report it.
NLOHMANN_VIEW_NOINLINE inline void check_utf8_scan(const unsigned char* p, std::size_t n)
{
const unsigned char* const e = p + n;
const unsigned char* q = p;
for (;;)
{
q = scan_string_run<true>(q, e);
if (q == e)
{
return;
}
if (*q >= 0x80)
{
check_utf8_bytewise(p, n, static_cast<std::size_t>(q - p));
return; // not reached: the scan stopped at ill-formed UTF-8
}
++q; // a quote, backslash, or control character
}
}
inline void check_utf8(const char* s, std::size_t n)
{
const auto* const p = reinterpret_cast<const unsigned char*>(s); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
if (NLOHMANN_VIEW_UNLIKELY(n >= 32))
{
check_utf8_scan(p, n);
return;
}
// short strings: the vector scan costs more than it saves; (the loop is
// inlined with a constant start, so this path is the plain byte-wise check)
check_utf8_bytewise(p, n, 0);
}
/*!
@brief the edits of an editable basic_json_document
@@ -157,7 +120,7 @@ class editor
node* const o = own(object);
if (o->kind != node_kind::object && o->kind != node_kind::null)
{
throw_type_error(site_of(object), exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", object.type_name());
throw_type_error(exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", object.type_name());
}
check_utf8(key.data(), key.size());
const encoded e = encode(std::forward<V>(value));
@@ -208,9 +171,9 @@ class editor
node* const a = own(array);
if (a->kind != node_kind::array)
{
throw_type_error(site_of(array), exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", array.type_name());
throw_type_error(exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", array.type_name());
}
check_index(a, idx, a->len);
check_index(idx, a->len);
const encoded e = encode(std::forward<V>(value));
node* const slot = const_cast<node*>(nav::value(element_at<true>(m_doc, a, idx))); // NOLINT(cppcoreguidelines-pro-type-const-cast)
assign(slot, e, a, true);
@@ -225,7 +188,7 @@ class editor
node* const a = own(array);
if (a->kind != node_kind::array && a->kind != node_kind::null)
{
throw_type_error(site_of(array), exception_id::push_back_wrong_type, "cannot use push_back() with ", array.type_name());
throw_type_error(exception_id::push_back_wrong_type, "cannot use push_back() with ", array.type_name());
}
const encoded e = encode(std::forward<V>(value));
if (a->kind == node_kind::null)
@@ -249,9 +212,9 @@ class editor
node* const a = own(array);
if (a->kind != node_kind::array)
{
throw_type_error(site_of(array), exception_id::insert_wrong_type, "cannot use insert() with ", array.type_name());
throw_type_error(exception_id::insert_wrong_type, "cannot use insert() with ", array.type_name());
}
check_index(a, idx, a->len + 1);
check_index(idx, a->len + 1);
const encoded e = encode(std::forward<V>(value));
node* const slot = new_slot(e);
node* const h = block_of(m_doc, a, 1);
@@ -269,7 +232,7 @@ class editor
node* const o = own(object);
if (o->kind != node_kind::object)
{
throw_type_error(site_of(object), exception_id::erase_wrong_type, "cannot use erase() with ", object.type_name());
throw_type_error(exception_id::erase_wrong_type, "cannot use erase() with ", object.type_name());
}
for (const node* k = nav::first(m_doc, o), *end = nav::end(m_doc, o); k != end; k = document_data::after(k + 1))
{
@@ -287,9 +250,9 @@ class editor
node* const a = own(array);
if (a->kind != node_kind::array)
{
throw_type_error(site_of(array), exception_id::erase_wrong_type, "cannot use erase() with ", array.type_name());
throw_type_error(exception_id::erase_wrong_type, "cannot use erase() with ", array.type_name());
}
check_index(a, idx, a->len);
check_index(idx, a->len);
node* const h = block_of(m_doc, a, 0);
std::memmove(h + 1 + idx, h + 2 + idx, (h->next - 2 - idx) * sizeof(node));
--h->next;
@@ -316,11 +279,11 @@ class editor
return const_cast<node*>(v.m_node); // NOLINT(cppcoreguidelines-pro-type-const-cast): the nodes belong to this document
}
void check_index(const node* array, std::size_t idx, std::size_t limit) const
static void check_index(std::size_t idx, std::size_t limit)
{
if (idx >= limit)
{
throw_array_index_out_of_range(error_site(&m_doc, array), idx);
throw_array_index_out_of_range(idx);
}
}
+8 -315
View File
@@ -10,15 +10,11 @@
#include <algorithm> // min
#include <cstddef> // size_t
#include <string> // string, to_string
#include <vector> // vector
#include <string> // string
#include <nlohmann/json.hpp>
#include <nlohmann/detail/view/builder.hpp>
#include <nlohmann/detail/view/document_data.hpp>
#include <nlohmann/detail/view/macro_scope.hpp>
#include <nlohmann/detail/view/materialize.hpp>
#include <nlohmann/detail/view/node.hpp>
NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail
@@ -26,292 +22,8 @@ namespace detail
namespace view
{
/*!
@brief the value an exception is about
basic_json passes the value to the exception, which then names its path
(JSON_DIAGNOSTICS) and its bytes in the source (JSON_DIAGNOSTIC_POSITIONS). A
view's value has no parent pointer and no stored positions: both are found
from the document and the node, when the exception is thrown (so only the
error path pays for them). Without the two macros, a site is empty.
*/
class error_site
{
public:
error_site() noexcept = default;
#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS
error_site(const document_data* d, const node* n) noexcept
: m_doc(d), m_node(n)
{}
const document_data* doc() const noexcept
{
return m_doc;
}
const node* value() const noexcept
{
return m_node;
}
private:
const document_data* m_doc = nullptr;
const node* m_node = nullptr;
#else
error_site(const document_data* /*d*/, const node* /*n*/) noexcept {}
#endif
};
/// the site of a view's value (the views make this a friend)
struct view_access
{
template<typename View>
static error_site site(const View& v) noexcept
{
return error_site(v.m_doc, v.m_node);
}
};
template<typename View>
error_site site_of(const View& v) noexcept
{
static_assert(is_json_view<View>::value, "the site of a value is only defined for views");
return view_access::site(v);
}
#if JSON_DIAGNOSTICS
/*!
@brief the JSON pointer tokens from the root of document @a d to node @a target
Nodes have no parent index, so the path is found by descending from the root:
before the first edit, only into the subtree that contains the target (a
container spans the next `next` nodes); after edits, elements can live in
other sequences, so every container is searched. Iterative (the nesting depth
is limited by memory only). False if the target is not found (then tokens is
unspecified).
*/
template<bool Editable>
bool value_path(const document_data& d, const node* target, std::vector<std::string>& tokens)
{
using nav = navigation<Editable>;
struct frame
{
const node* pos;
const node* end;
std::size_t index;
bool object;
};
const node* const root = d.nodes;
if (target == root)
{
return true;
}
if (!is_container(*root))
{
return false;
}
std::vector<frame> stack;
stack.push_back(frame{nav::first(d, root), nav::end(d, root), 0, root->kind == node_kind::object});
// (tokens[i] leads from stack[i] to stack[i + 1], or to the target)
for (;;)
{
// (f is not used after the stack changes: pop_back() below, push_back() at the end)
frame& f = stack.back();
if (f.pos == f.end)
{
stack.pop_back();
if (stack.empty())
{
return false;
}
tokens.pop_back();
continue;
}
const node* entry = f.pos;
std::string token;
if (f.object)
{
// (as basic_json: the key up to a null byte)
token.assign(d.str(*entry), entry->len);
const std::size_t nul = token.find('\0');
if (nul != std::string::npos)
{
token.resize(nul);
}
++entry;
}
else
{
token = std::to_string(f.index);
}
const node* const child = nav::value(entry);
if (child == target)
{
tokens.push_back(token);
return true;
}
f.pos = document_data::after(entry);
++f.index;
if (is_container(*child) && (nav::editable() || (target > child && target < child + child->next)))
{
tokens.push_back(token);
stack.push_back(frame{nav::first(d, child), nav::end(d, child), 0, child->kind == node_kind::object});
}
}
}
#endif
#if JSON_DIAGNOSTIC_POSITIONS
/*!
@brief the bytes [start, end) of node @a n in the source, as materialize() sets them
Scalars as position_source::set_scalar; an array or object from its opening
bracket to its closing bracket, which is found by following the chain of last
elements to a scalar (or an empty container) and scanning from there to the
closing brackets. None (npos) for values written by edits, and for containers
whose last element has none, as with materialize().
*/
template<bool Editable>
void value_range(const document_data& d, const node* n, std::size_t& start, std::size_t& end)
{
using nav = navigation<Editable>;
position_source p;
start = std::string::npos;
end = std::string::npos;
if (!is_container(*n))
{
p.set_scalar(d, *n);
start = p.start;
end = p.end;
return;
}
if ((n->flags & node_flags::is_new) != 0 || n->off >= d.size)
{
return;
}
// the open brackets: n, then the last element as long as it is a non-empty container
std::size_t closing = 0;
std::size_t pos = std::string::npos;
for (const node* c = n;;)
{
if (!is_container(*c))
{
p.set_scalar(d, *c);
pos = p.end;
break;
}
if ((c->flags & node_flags::is_new) != 0 || c->off >= d.size)
{
return;
}
++closing;
const node* const first = nav::first(d, c);
const node* const last_end = nav::end(d, c);
if (first == last_end)
{
pos = std::size_t{c->off} + 1;
break;
}
const std::size_t value_offset = c->kind == node_kind::object ? 1 : 0;
const node* last = first;
for (const node* e = document_data::after(first + value_offset); e != last_end; e = document_data::after(e + value_offset))
{
last = e;
}
c = nav::value(last + value_offset);
}
for (; closing != 0 && pos != std::string::npos; --closing)
{
pos = position_source::container_end(d.base[0], d.size, pos);
}
if (pos != std::string::npos)
{
start = n->off;
end = pos;
}
}
#endif
/// the context basic_json puts in front of the message of an exception about
/// a value: "(/path) " with JSON_DIAGNOSTICS (none for the root), then
/// "(bytes a-b) " with JSON_DIAGNOSTIC_POSITIONS (with JSON_DIAGNOSTICS, only
/// if there is a path)
///
/// This and the throw helpers that take an error_site are templates, so that
/// the path and range code is instantiated only where an exception can be
/// thrown: instantiated in every translation unit (as a non-template inline
/// function makes it), it changed GCC's early inlining of unrelated view
/// functions, so that GCC 13 no longer inlined a recursive walk over a view
/// into itself (2.7 times the instructions).
#if JSON_DIAGNOSTICS || JSON_DIAGNOSTIC_POSITIONS
template<typename Unused = void>
std::string diagnostics(const error_site site)
{
if (site.doc() == nullptr || site.value() == nullptr)
{
return "";
}
const document_data& d = *site.doc();
const bool edited = d.edits != nullptr;
std::string context;
#if JSON_DIAGNOSTICS
std::vector<std::string> tokens;
const bool found = edited ? value_path<true>(d, site.value(), tokens) : value_path<false>(d, site.value(), tokens);
if (!found || tokens.empty())
{
return "";
}
std::string path;
for (const std::string& token : tokens)
{
path.push_back('/');
path += detail::escape(token);
}
context = concat('(', path, ") ");
#endif
#if JSON_DIAGNOSTIC_POSITIONS
std::size_t start = 0;
std::size_t end = 0;
if (edited)
{
value_range<true>(d, site.value(), start, end);
}
else
{
value_range<false>(d, site.value(), start, end);
}
if (start != std::string::npos && end != std::string::npos)
{
context += concat("(bytes ", std::to_string(start), "-", std::to_string(end), ") ");
}
#endif
return context;
}
#else
template<typename Unused = void>
std::string diagnostics(const error_site /*site*/)
{
return "";
}
#endif
/// the context of an exception about view @a v (see exception_prefix())
template<typename View>
std::string diagnostics_of(const View& v)
{
return diagnostics<>(site_of(v));
}
// Exceptions are thrown out of line, so that the accessors that may throw stay
// small enough to be inlined. The overloads with an error_site put its context
// in front of the message.
//
// The overloads without one are for exceptions that are not about a value of a
// document: limits and invalid input of the editing and image functions, a
// discarded view (it has no node), a view of another document (its path would
// be one in that document), and those that basic_json creates without a
// context too (the errors of json_pointer, invalid UTF-8). The one exception
// about a value that has no site is key(), see view_iterator::key().
// small enough to be inlined.
[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_type_error(exception_id id, const char* prefix, const char* type)
{
@@ -333,24 +45,6 @@ std::string diagnostics_of(const View& v)
NLOHMANN_VIEW_THROW(invalid_iterator::create(id, msg, nullptr));
}
template<typename Unused = void>
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_type_error(const error_site site, exception_id id, const char* prefix, const char* type)
{
NLOHMANN_VIEW_THROW(type_error::create(id, concat(diagnostics<>(site), prefix, type), nullptr));
}
template<typename Unused = void>
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_out_of_range(const error_site site, exception_id id, const std::string& msg)
{
NLOHMANN_VIEW_THROW(out_of_range::create(id, concat(diagnostics<>(site), msg), nullptr));
}
template<typename Unused = void>
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_invalid_iterator(const error_site site, exception_id id, const char* msg)
{
NLOHMANN_VIEW_THROW(invalid_iterator::create(id, concat(diagnostics<>(site), msg), nullptr));
}
/// a parse error without a position (as those of json_pointer)
[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_parse_error(exception_id id, const std::string& msg)
{
@@ -359,23 +53,22 @@ template<typename Unused = void>
/// out_of_range.401: the array index @a idx is not below the size
template<typename Index>
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_out_of_range(const error_site site, const Index idx)
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_out_of_range(const Index idx)
{
throw_out_of_range(site, exception_id::array_index_out_of_range, concat("array index ", std::to_string(idx), " is out of range"));
throw_out_of_range(exception_id::array_index_out_of_range, concat("array index ", std::to_string(idx), " is out of range"));
}
/// out_of_range.402: the JSON Pointer token '-' of an array of @a size elements
template<typename Unused = void>
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_past_end(const error_site site, std::size_t size)
[[noreturn]] NLOHMANN_VIEW_NOINLINE inline void throw_array_index_past_end(std::size_t size)
{
throw_out_of_range(site, exception_id::pointer_past_the_end_index, concat("array index '-' (", std::to_string(size), ") is out of range"));
throw_out_of_range(exception_id::pointer_past_the_end_index, concat("array index '-' (", std::to_string(size), ") is out of range"));
}
/// out_of_range.403: the object has no member @a key
template<typename Key>
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_key_not_found(const error_site site, const Key& key)
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_key_not_found(const Key& key)
{
throw_out_of_range(site, exception_id::key_not_found, concat("key '", key, "' not found"));
throw_out_of_range(exception_id::key_not_found, concat("key '", key, "' not found"));
}
/// SAX handler of the error replay: accepts every event without storing it,
+18 -100
View File
@@ -258,81 +258,6 @@ struct number_token
bool is_float; ///< a fraction or an exponent
};
/// advance over ASCII digits, eight bytes at a time
inline const unsigned char* digit_run(const unsigned char* p, const unsigned char* e) noexcept
{
constexpr std::uint64_t ones = 0x0101010101010101ull;
while (e - p >= 8)
{
// (the digits become 0..9; a byte above 9, or with bit 7 set, is no digit. A carry
// out of a byte with bit 7 set only affects the bytes above, after the first one
// that count_trailing_zeros finds.)
const std::uint64_t x = read_eight_bytes(p) ^ (ones * 0x30u);
const std::uint64_t not_digit = ((x + (ones * 0x76u)) | x) & (ones * 0x80u);
if (not_digit != 0)
{
return p + (static_cast<std::size_t>(count_trailing_zeros(not_digit)) / 8);
}
p += 8;
}
while (p != e && is_digit(*p))
{
++p;
}
return p;
}
/// whether the n bytes at p are ASCII digits (n > 0)
inline bool all_digits(const unsigned char* p, std::size_t n) noexcept
{
const auto digits = [](const unsigned char* q)
{
constexpr std::uint64_t ones = 0x0101010101010101ull;
const std::uint64_t x = read_eight_bytes(q) ^ (ones * 0x30u);
return (((x + (ones * 0x76u)) | x) & (ones * 0x80u)) == 0;
};
if (n >= 8)
{
for (std::size_t i = 0; i + 8 < n; i += 8)
{
if (!digits(p + i))
{
return false;
}
}
return digits(p + n - 8); // (overlapping the bytes before: they are digits)
}
for (std::size_t i = 0; i < n; ++i)
{
if (!is_digit(p[i]))
{
return false;
}
}
return true;
}
/// Whether a float token has the shape that the parser records for most of
/// them: [-]digits.digits, with the numbers of digits of the node's layout
/// (extra). Then the token is a number of that layout, and the general scan
/// that follows is not needed.
inline bool plain_float_token(const unsigned char* s, std::size_t len, std::uint16_t extra) noexcept
{
const std::size_t int_digits = extra & 0xFFu;
const std::size_t frac_digits = extra >> 8u;
if (int_digits == 0 || int_digits >= 255 || frac_digits == 0 || frac_digits >= 255 || len == 0)
{
return false;
}
const std::size_t negative = *s == '-' ? 1 : 0;
if (len != negative + int_digits + 1 + frac_digits)
{
return false;
}
const unsigned char* const p = s + negative;
return !(int_digits > 1 && *p == '0') && p[int_digits] == '.' && all_digits(p, int_digits) && all_digits(p + int_digits + 1, frac_digits);
}
/// whether [s, s + len) is a JSON number (the grammar the parser accepts)
inline bool scan_number_token(const unsigned char* s, std::size_t len, number_token& t)
{
@@ -351,7 +276,11 @@ inline bool scan_number_token(const unsigned char* s, std::size_t len, number_to
}
else if (*p >= '1' && *p <= '9')
{
p = digit_run(p + 1, e);
do
{
++p;
}
while (p != e && is_digit(*p));
}
else
{
@@ -363,7 +292,10 @@ inline bool scan_number_token(const unsigned char* s, std::size_t len, number_to
if (p != e && *p == '.')
{
const unsigned char* const f0 = ++p;
p = digit_run(p, e);
while (p != e && is_digit(*p))
{
++p;
}
if (p == f0)
{
return false;
@@ -474,15 +406,10 @@ struct float_range
template<typename Check>
bool check_distinct_ranges(std::vector<byte_range>& ranges, Check check)
{
// (save() writes the ranges in the order of the nodes, which is sorted already)
const auto before = [](const byte_range & a, const byte_range & b)
std::sort(ranges.begin(), ranges.end(), [](const byte_range & a, const byte_range & b)
{
return a.off != b.off ? a.off < b.off : a.len < b.len;
};
if (!std::is_sorted(ranges.begin(), ranges.end(), before))
{
std::sort(ranges.begin(), ranges.end(), before);
}
});
std::size_t end = 0;
for (std::size_t i = 0; i < ranges.size(); ++i)
{
@@ -504,8 +431,7 @@ bool check_distinct_ranges(std::vector<byte_range>& ranges, Check check)
/// record layouts that match it), tokens must not overlap.
inline bool check_float_ranges(std::vector<float_range>& ranges, const unsigned char* text)
{
// (save() writes the tokens in the order of the nodes, which is sorted already)
const auto before = [](const float_range & a, const float_range & b)
std::sort(ranges.begin(), ranges.end(), [](const float_range & a, const float_range & b)
{
if (a.off != b.off)
{
@@ -516,11 +442,7 @@ inline bool check_float_ranges(std::vector<float_range>& ranges, const unsigned
return a.len < b.len;
}
return a.extra < b.extra;
};
if (!std::is_sorted(ranges.begin(), ranges.end(), before))
{
std::sort(ranges.begin(), ranges.end(), before);
}
});
std::size_t end = 0;
std::size_t i = 0;
while (i < ranges.size())
@@ -530,18 +452,14 @@ inline bool check_float_ranges(std::vector<float_range>& ranges, const unsigned
{
return false;
}
number_token t{};
const unsigned char* const s = text + r.off;
std::uint16_t layout = r.extra;
if (!plain_float_token(s, r.len, r.extra))
if (!scan_number_token(s, r.len, t) || !float_token_finite(s, r.len, t))
{
number_token t{};
if (!scan_number_token(s, r.len, t) || !float_token_finite(s, r.len, t))
{
return false;
}
// the digit layout the parser records (or "many", as compaction writes it)
layout = float_layout(t);
return false;
}
// the digit layout the parser records (or "many", as compaction writes it)
const std::uint16_t layout = float_layout(t);
for (; i < ranges.size() && ranges[i].off == r.off && ranges[i].len == r.len; ++i)
{
if (ranges[i].extra != layout && ranges[i].extra != 0xFFFFu)
+1 -4
View File
@@ -105,10 +105,7 @@ class view_iterator
}
/// the key of the current object member; throws invalid_iterator.207 for
/// other iterators, like basic_json's iterators (but without the context
/// JSON_DIAGNOSTICS and JSON_DIAGNOSTIC_POSITIONS add: the iterator does not
/// know the value it iterates over, and keeping it would make iterators
/// larger, which keeps GCC from inlining recursive walks)
/// other iterators, like basic_json's iterators
string_view_t key() const
{
if (NLOHMANN_VIEW_UNLIKELY(m_value_offset == 0))
+4 -5
View File
@@ -44,7 +44,6 @@ index_status array_index(const StringType& s, std::size_t& idx) noexcept
}
/// throws the exception json_pointer::array_index throws for this status
/// (without a context, as json_pointer::array_index)
template<typename StringType>
[[noreturn]] NLOHMANN_VIEW_NOINLINE void throw_array_index_error(index_status status, const StringType& s)
{
@@ -84,7 +83,7 @@ View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode)
{
if (mode == pointer_mode::checked)
{
throw_key_not_found(site_of(cur), token);
throw_key_not_found(token);
}
return View();
}
@@ -96,7 +95,7 @@ View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode)
{
if (throwing)
{
throw_array_index_past_end(site_of(cur), cur.size());
throw_array_index_past_end(cur.size());
}
return View();
}
@@ -115,7 +114,7 @@ View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode)
{
if (mode == pointer_mode::checked)
{
throw_array_index_out_of_range(site_of(cur), idx);
throw_array_index_out_of_range(idx);
}
return View();
}
@@ -125,7 +124,7 @@ View resolve_pointer(View cur, const Tokens& tokens, pointer_mode mode)
{
if (throwing)
{
throw_out_of_range(site_of(cur), exception_id::pointer_unresolved, concat("unresolved reference token '", token, "'"));
throw_out_of_range(exception_id::pointer_unresolved, concat("unresolved reference token '", token, "'"));
}
return View();
}
-7
View File
@@ -29,13 +29,6 @@
// NLOHMANN_VIEW_NO_TARGET_ATTRIBUTE (set by the GCC module interface, where GCC
// ignores the target attribute and then rejects the SSSE3 intrinsics) keeps the
// check portable without disabling the SSE2 code.
//
// The vector UTF-8 check (utf8_lookup4) is adapted from simdjson
// (https://github.com/simdjson/simdjson, Daniel Lemire, Geoff Langdale, John
// Keiser, and contributors, available under the Apache 2.0 and MIT licenses;
// used here under the MIT license): the "lookup4" algorithm and tables of
// J. Keiser and D. Lemire, "Validating UTF-8 In Less Than One Instruction Per
// Byte" (2021).
#if !defined(JSON_NO_SIMD) && defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) && NLOHMANN_VIEW_LITTLE_ENDIAN
#include <arm_neon.h>
#define NLOHMANN_VIEW_NEON 1
+3 -3
View File
@@ -60,7 +60,7 @@ NLOHMANN_VIEW_ALWAYS_INLINE T arithmetic_value(const document_data& d, const nod
case value_t::binary:
case value_t::discarded:
default:
throw_type_error(error_site(&d, &n), exception_id::type_mismatch, "type must be number, but is ", value_type_name(static_cast<value_t>(n.kind)));
throw_type_error(exception_id::type_mismatch, "type must be number, but is ", value_type_name(static_cast<value_t>(n.kind)));
}
}
@@ -70,7 +70,7 @@ std::vector<U, A> vector_value(const View& v)
{
if (NLOHMANN_VIEW_UNLIKELY(!v.is_array()))
{
throw_type_error(site_of(v), exception_id::type_mismatch, "type must be array, but is ", v.type_name());
throw_type_error(exception_id::type_mismatch, "type must be array, but is ", v.type_name());
}
std::vector<U, A> r;
r.reserve(v.size());
@@ -88,7 +88,7 @@ Map map_value(const View& v)
{
if (NLOHMANN_VIEW_UNLIKELY(!v.is_object()))
{
throw_type_error(site_of(v), exception_id::type_mismatch, "type must be object, but is ", v.type_name());
throw_type_error(exception_id::type_mismatch, "type must be object, but is ", v.type_name());
}
Map r;
for (auto it = v.begin(); it != v.end(); ++it)
+26 -34
View File
@@ -285,7 +285,7 @@ class basic_json_view
{
return {};
}
detail::view::throw_type_error(site(), detail::exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", type_name());
detail::view::throw_type_error(detail::exception_id::subscript_wrong_type, "cannot use operator[] with a string argument with ", type_name());
}
return lookup(key);
}
@@ -311,7 +311,7 @@ class basic_json_view
{
return {};
}
detail::view::throw_type_error(site(), detail::exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", type_name());
detail::view::throw_type_error(detail::exception_id::subscript_wrong_type, "cannot use operator[] with a numeric argument with ", type_name());
}
return idx < m_node->len ? element(idx) : basic_json_view();
}
@@ -344,12 +344,12 @@ class basic_json_view
{
if (NLOHMANN_VIEW_UNLIKELY(!is_object()))
{
detail::view::throw_type_error(site(), detail::exception_id::at_wrong_type, "cannot use at() with ", type_name());
detail::view::throw_type_error(detail::exception_id::at_wrong_type, "cannot use at() with ", type_name());
}
const basic_json_view r = lookup(key);
if (NLOHMANN_VIEW_UNLIKELY(r.is_discarded()))
{
detail::view::throw_key_not_found(site(), std::string(key.data(), key.size()));
detail::view::throw_key_not_found(std::string(key.data(), key.size()));
}
return r;
}
@@ -370,11 +370,11 @@ class basic_json_view
{
if (NLOHMANN_VIEW_UNLIKELY(!is_array()))
{
detail::view::throw_type_error(site(), detail::exception_id::at_wrong_type, "cannot use at() with ", type_name());
detail::view::throw_type_error(detail::exception_id::at_wrong_type, "cannot use at() with ", type_name());
}
if (NLOHMANN_VIEW_UNLIKELY(idx >= m_node->len))
{
detail::view::throw_array_index_out_of_range(site(), idx);
detail::view::throw_array_index_out_of_range(idx);
}
return element(idx);
}
@@ -402,7 +402,7 @@ class basic_json_view
{
if (NLOHMANN_VIEW_UNLIKELY(!is_object()))
{
detail::view::throw_type_error(site(), detail::exception_id::value_wrong_type, "cannot use value() with ", type_name());
detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name());
}
const basic_json_view r = lookup(key);
return r.is_discarded() ? default_value : r.template get<T>();
@@ -412,7 +412,7 @@ class basic_json_view
{
if (NLOHMANN_VIEW_UNLIKELY(!is_object()))
{
detail::view::throw_type_error(site(), detail::exception_id::value_wrong_type, "cannot use value() with ", type_name());
detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name());
}
// (the default is made only if there is no such member)
const basic_json_view r = lookup(key);
@@ -427,7 +427,7 @@ class basic_json_view
{
if (NLOHMANN_VIEW_UNLIKELY(!is_structured()))
{
detail::view::throw_type_error(site(), detail::exception_id::value_wrong_type, "cannot use value() with ", type_name());
detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name());
}
const basic_json_view r = detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::value);
return r.is_discarded() ? default_value : r.template get<T>();
@@ -437,7 +437,7 @@ class basic_json_view
{
if (NLOHMANN_VIEW_UNLIKELY(!is_structured()))
{
detail::view::throw_type_error(site(), detail::exception_id::value_wrong_type, "cannot use value() with ", type_name());
detail::view::throw_type_error(detail::exception_id::value_wrong_type, "cannot use value() with ", type_name());
}
const basic_json_view r = detail::view::resolve_pointer(*this, detail::json_pointer_access::reference_tokens(ptr), detail::view::pointer_mode::value);
return r.is_discarded() ? string_t(default_value) : r.template get<string_t>();
@@ -450,7 +450,7 @@ class basic_json_view
const iterator it = begin();
if (NLOHMANN_VIEW_UNLIKELY(it == end()))
{
detail::view::throw_invalid_iterator(site(), detail::exception_id::iterator_value_unavailable, "cannot get value");
detail::view::throw_invalid_iterator(detail::exception_id::iterator_value_unavailable, "cannot get value");
}
return *it;
}
@@ -606,7 +606,7 @@ class basic_json_view
{
if (NLOHMANN_VIEW_UNLIKELY(!is_string()))
{
detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be string, but is ", type_name());
detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be string, but is ", type_name());
}
return {m_doc->str(*m_node), m_node->len};
}
@@ -618,7 +618,7 @@ class basic_json_view
{
if (NLOHMANN_VIEW_UNLIKELY(!is_number()))
{
detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be number, but is ", type_name());
detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be number, but is ", type_name());
}
return {m_doc->str(*m_node), detail::view::number_length(*m_node)};
}
@@ -763,20 +763,12 @@ class basic_json_view
template<typename, view_mode> friend class basic_json_document;
template<typename, view_mode> friend class basic_json_view;
template<typename, typename> friend class detail::view::editor;
template<typename, typename> friend class detail::view::view_side;
friend struct detail::view::view_access;
friend iterator;
basic_json_view(const document_data* d, const node* n) noexcept
: m_doc(d), m_node(n)
{}
/// this value as the context of an exception
detail::view::error_site site() const noexcept
{
return detail::view::error_site(m_doc, m_node);
}
using json_side_t = detail::view::json_side<BasicJsonType, string_view_t>;
static detail::view::view_side<BasicJsonType, basic_json_view> side(const basic_json_view& v) noexcept
@@ -883,7 +875,7 @@ class basic_json_view
{
if (NLOHMANN_VIEW_UNLIKELY(!is_boolean()))
{
detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be boolean, but is ", type_name());
detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be boolean, but is ", type_name());
}
return (m_node->flags & detail::view::node_flags::is_true) != 0;
}
@@ -893,7 +885,7 @@ class basic_json_view
{
if (NLOHMANN_VIEW_UNLIKELY(m_node == nullptr))
{
detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be number, but is ", type_name());
detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be number, but is ", type_name());
}
return detail::view::arithmetic_value<T, BasicJsonType>(*m_doc, *m_node);
}
@@ -902,7 +894,7 @@ class basic_json_view
{
if (NLOHMANN_VIEW_UNLIKELY(!is_null()))
{
detail::view::throw_type_error(site(), detail::exception_id::type_mismatch, "type must be null, but is ", type_name());
detail::view::throw_type_error(detail::exception_id::type_mismatch, "type must be null, but is ", type_name());
}
return nullptr;
}
@@ -1316,7 +1308,7 @@ class basic_json_document
template<typename I, typename V, typename std::enable_if<detail::view::is_non_bool_integral<I>::value, int>::type = 0>
view_type set(view_type array, I idx, V && value)
{
return editor().set(array, index(array, idx), std::forward<V>(value));
return editor().set(array, index(idx), std::forward<V>(value));
}
/// set the value at a JSON pointer: its parent must exist; an object
@@ -1353,7 +1345,7 @@ class basic_json_document
const std::size_t idx = pointer_index(token);
if (idx >= 0xFFFFFFFFu)
{
detail::view::throw_array_index_out_of_range(detail::view::site_of(parent), idx);
detail::view::throw_array_index_out_of_range(idx);
}
for (std::size_t i = 0; i < idx; ++i)
{
@@ -1387,7 +1379,7 @@ class basic_json_document
template<typename I, typename V, typename std::enable_if<detail::view::is_non_bool_integral<I>::value, int>::type = 0>
view_type insert(view_type array, I idx, V && value)
{
return editor().insert(array, index(array, idx), std::forward<V>(value));
return editor().insert(array, index(idx), std::forward<V>(value));
}
/// remove all members with this key; returns their number
@@ -1400,7 +1392,7 @@ class basic_json_document
template<typename I, typename std::enable_if<detail::view::is_non_bool_integral<I>::value, int>::type = 0>
void erase(view_type array, I idx)
{
editor().erase(array, index(array, idx));
editor().erase(array, index(idx));
}
/// remove the value at a JSON pointer; returns the number of removed
@@ -1434,25 +1426,25 @@ class basic_json_document
return detail::view::editor<BasicJsonType, view_type>(*m_data);
}
/// an index into @a array (out_of_range.401 if negative)
/// an index (out_of_range.401 if negative)
template<typename I>
static std::size_t index(const view_type& array, I idx)
static std::size_t index(I idx)
{
return index(array, idx, std::is_signed<I> {});
return index(idx, std::is_signed<I> {});
}
template<typename I>
static std::size_t index(const view_type& array, I idx, std::true_type /*signed*/)
static std::size_t index(I idx, std::true_type /*signed*/)
{
if (idx < 0)
{
detail::view::throw_array_index_out_of_range(detail::view::site_of(array), idx);
detail::view::throw_array_index_out_of_range(idx);
}
return static_cast<std::size_t>(idx);
}
template<typename I>
static std::size_t index(const view_type& /*array*/, I idx, std::false_type /*unsigned*/)
static std::size_t index(I idx, std::false_type /*unsigned*/)
{
return static_cast<std::size_t>(idx);
}
+36 -43
View File
@@ -2720,7 +2720,7 @@ JSON_HEDLEY_DIAGNOSTIC_POP
return ej_pair.second == j; \
}); \
if (it != std::end(m)) e = it->first; \
else ::nlohmann::detail::templated_json_throw<nlohmann::detail::out_of_range>(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range, nlohmann::detail::concat(nlohmann::detail::exception_prefix(j), "enum value out of range for " #ENUM_TYPE ": ", nlohmann::detail::from_json_dump(j)), nlohmann::detail::exception_context(j))); \
else ::nlohmann::detail::templated_json_throw<nlohmann::detail::out_of_range>(nlohmann::detail::out_of_range::create(nlohmann::detail::exception_id::value_out_of_range, nlohmann::detail::concat("enum value out of range for " #ENUM_TYPE ": ", nlohmann::detail::from_json_dump(j)), nlohmann::detail::exception_context(j))); \
}
// Ugly macros to avoid uglier copy-paste when specializing basic_json. They
@@ -5928,28 +5928,6 @@ std::nullptr_t exception_context(const View& /*unused*/) noexcept
return nullptr;
}
namespace view
{
// the path and bytes of the value of a view (defined in <nlohmann/detail/view/errors.hpp>)
template<typename View>
std::string diagnostics_of(const View& v);
} // namespace view
// what goes in front of the message of an exception about j: nothing for a
// basic_json value (exception_context() passes the value itself), the context
// basic_json would add (JSON_DIAGNOSTICS, JSON_DIAGNOSTIC_POSITIONS) for a view
template<typename BasicJsonType, enable_if_t<is_basic_json<BasicJsonType>::value, int> = 0>
const char* exception_prefix(const BasicJsonType& /*unused*/) noexcept
{
return "";
}
template<typename View, enable_if_t<is_json_view<View>::value, int> = 0>
std::string exception_prefix(const View& v)
{
return view::diagnostics_of(v);
}
/*!
@brief throws because @a j does not have the type a conversion expects
@param[in] expected the expected type(s), e.g. "array" or "binary or array"
@@ -5961,7 +5939,7 @@ JSON_HEDLEY_NO_RETURN JSON_HEDLEY_NEVER_INLINE void throw_type_must_be(const cha
{
static_cast<void>(expected); // unused when JSON_NOEXCEPTION is defined
static_cast<void>(j);
JSON_THROW(type_error::create(exception_id::type_mismatch, concat(exception_prefix(j), "type must be ", expected, ", but is ", j.type_name()), exception_context(j)));
JSON_THROW(type_error::create(exception_id::type_mismatch, concat("type must be ", expected, ", but is ", j.type_name()), exception_context(j)));
}
/*!
@@ -10992,13 +10970,6 @@ NLOHMANN_JSON_NAMESPACE_END
// use std::from_chars or std::strtold. They are free functions so the lexer
// stays focused on scanning (see lexer::convert_number()) and so that other
// parsers of JSON text can convert tokens exactly like it does.
//
// The Eisel-Lemire conversion (eisel_lemire()), the digit comparison for the
// cases it cannot decide, and the eight-digits-at-a-time helpers
// (is_eight_digits(), parse_eight_digits()) are adapted from fast_float
// (https://github.com/fastfloat/fast_float, Daniel Lemire and contributors,
// available under the MIT, Apache 2.0, and Boost Software licenses; used here
// under the MIT license). The table of powers of five is in pow5_table.hpp.
NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail
@@ -12107,13 +12078,6 @@ NLOHMANN_JSON_NAMESPACE_END
// NLOHMANN_JSON_NO_TARGET_ATTRIBUTE (for compilers that ignore the target
// attribute and then reject the SSSE3 intrinsics) keeps the check portable
// without disabling the SSE2 code.
//
// The vector UTF-8 check (utf8_lookup4) is adapted from simdjson
// (https://github.com/simdjson/simdjson, Daniel Lemire, Geoff Langdale, John
// Keiser, and contributors, available under the Apache 2.0 and MIT licenses;
// used here under the MIT license): the "lookup4" algorithm and tables of
// J. Keiser and D. Lemire, "Validating UTF-8 In Less Than One Instruction Per
// Byte" (2021).
#if !defined(JSON_NO_SIMD) && defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) && defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
#include <arm_neon.h>
#define JSON_SCAN_NEON 1
@@ -32531,11 +32495,6 @@ NLOHMANN_JSON_NAMESPACE_END
#include <type_traits> // conditional
// The shortest-digit conversion of doubles (see zmij.hpp) and the writing of
// its digits, including the conversion of the digits to text by Xiang JunBo
// and the SIMD instruction sequence by Dougall Johnson, are ported from Zmij
// by Victor Zverovich (https://github.com/vitaut/zmij, MIT license).
//
// SSE2 (every x86-64 CPU) and NEON (every 64-bit Arm CPU) convert the 16
// digits of a double at once (unless JSON_NO_SIMD selects the portable code)
#if !defined(JSON_NO_SIMD) && (defined(__x86_64__) || (defined(_M_X64) && !defined(_M_ARM64EC)))
@@ -36246,6 +36205,23 @@ class serializer
return;
}
// 17 to 20 digits: the first one to four, then twice eight
if (magnitude_fits_64_bits(abs_value, std::integral_constant<bool, magnitude_has_at_most_64_bits>()))
{
const auto value = static_cast<std::uint64_t>(abs_value);
const std::uint64_t top = value / 10000000000000000u; // 1 to 1844
const std::uint64_t low16 = value - (top * 10000000000000000u);
const std::uint64_t middle = low16 / 100000000u;
const std::uint64_t first = dtoa_impl::eight_digit_bytes(top);
const auto leading = static_cast<unsigned>(count_leading_zeros(first) / 8); // (first is not 0)
char* const p = buffer_ptr + n_chars;
dtoa_impl::store_msb_first(p, (first << (8 * leading)) + 0x3030303030303030u);
dtoa_impl::store_msb_first(p + 8 - leading, dtoa_impl::eight_digit_bytes(middle) + 0x3030303030303030u);
dtoa_impl::store_msb_first(p + 16 - leading, dtoa_impl::eight_digit_bytes(low16 - (middle * 100000000u)) + 0x3030303030303030u);
write_buffer_pos += n_chars + 24 - leading;
return;
}
n_chars += count_digits(abs_value);
// spare 1 byte for '\0'
@@ -36391,6 +36367,23 @@ class serializer
}
}
/// whether every number_magnitude_t fits 64 bits
static constexpr bool magnitude_has_at_most_64_bits = std::numeric_limits<number_magnitude_t>::is_specialized
&& std::numeric_limits<number_magnitude_t>::digits <= 64;
/// whether @a x fits 64 bits: always if number_magnitude_t has at most
/// 64 bits (selected by type, as the comparison would be reported as
/// always true), else compared
static bool magnitude_fits_64_bits(const number_magnitude_t& /*unused*/, std::true_type /*unused*/) noexcept
{
return true;
}
static bool magnitude_fits_64_bits(const number_magnitude_t& x, std::false_type /*unused*/) noexcept
{
return x <= 18446744073709551615u;
}
/*
* Helper function for dump_integer
*
+946 -2261
View File
File diff suppressed because it is too large. Load diff
+1 -27
View File
@@ -21,10 +21,6 @@ python3 tests/benchmarks/json_view/compare.py --data <json_test_data directory>
- The other libraries come from the system: pkg-config, or Homebrew (`brew install yyjson simdjson boost`). With
`--download`, pinned releases are downloaded instead and checked against their SHA-256. Without Boost headers (or
with `--no-boost`), the Boost.JSON columns are skipped, and the results say so.
- `--aligned` additionally builds and runs everything with all functions aligned to 64 bytes (`-mllvm
-align-all-functions=6` for Clang, `-falign-functions=64 -falign-loops=64` for GCC), and reports both result
tables as "default build" and "aligned build". Code layout alone can move a result by a few percent; a difference
that appears in only one of the two tables is a layout artifact. Off by default, which doubles the run time.
- `--corpus file...` adds files to the corpus benchmark, e.g. those of
[simdjson-data](https://github.com/simdjson/simdjson-data) or the
[yyjson benchmark](https://github.com/ibireme/yyjson_benchmark).
@@ -43,7 +39,7 @@ where `json_view` stands on another architecture; they are not meant for publica
## What is measured
`bench_view.cpp` runs these workloads on twitter, citm_catalog, canada, jeopardy, a single tweet (`status`), and a
`bench_view.cpp` runs four workloads on twitter, citm_catalog, canada, jeopardy, a single tweet (`status`), and a
JSON-RPC request (`rpc`):
| workload | what it does |
@@ -52,7 +48,6 @@ JSON-RPC request (`rpc`):
| traverse | parse, then visit every value, convert every number, touch every string and key |
| select | parse, then read a few fields per record (e.g. id, user name, and retweet count of each tweet) |
| dump | serialize a parsed document (compact) |
| parse+dump | build a fresh document, then serialize it (compact), in one timed call |
`bench_corpus.cpp` runs parse, traverse, and dump on any list of files, so that no library is tuned to a handful of
documents. Its dump also writes the numbers as they are in the input: `json_view` with `number_format::source`, and
@@ -77,32 +72,11 @@ call follows an untimed call of the same engine: otherwise the engine after `jso
cleaning up the tens of thousands of nodes `json::parse` just freed (with glibc, this made `json_view` look 1.7 times
slower on citm_catalog traverse).
`json_view` is an index into the source text, not a tree of owning nodes. Its like-for-like peers are yyjson and
simdjson DOM (immutable, indexed, no per-node allocation). Boost.JSON and `json::parse` build mutable, owning DOMs and
are reference points for what that costs, not equals; simdjson On-Demand is lazy and forward-only, so it is the other
way round: it has no random access to pay for. All engines validate UTF-8 and syntax by default.
Number conversion differs: `json_view` converts integers while parsing but converts floating-point numbers lazily,
when they are read or dumped; yyjson (default flags) and simdjson DOM convert every number while parsing. The cost of
float conversion therefore appears in the traverse and dump rows for `json_view`, and in the parse row for the others.
For float-heavy documents (canada), the fair reading is a sum of parse and use: the `traverse` workload already
includes the parse, and `parse+dump` measures a fresh document and its dump in one timed call; the `dump` row alone
leaves the conversion in `json_view` and out of the others. Eager conversion at parse time was prototyped as an opt-in
and measured: canada traverse (with its parse) was 4-7 % faster, but parse+dump 11-30 % slower and parse 36-90 % slower. It was
dropped, so lazy conversion is the only mode.
`json_view` can also be edited: `json_editable_document` has the same read API, and the `(editable)` rows in
`bench_view` measure it on the same workloads. They are the fair counterpart of Boost.JSON and `json::parse`, which are
mutable too; the plain `json_view` rows are the counterpart of yyjson and simdjson DOM, which are not. `bench_edit` adds
the read-modify-write workloads. Read the tables with that in mind: an immutable-only library is not compared with an
editable `json_view` unless it says so.
The engines do not all offer the same features, which the numbers should be read with:
| engine | document | random access | editable | notes |
|---|---|---|---|---|
| `json_view` | immutable index into the text | yes | no | a fresh document per parse; "reused" parses into the same document |
| `json_view` (editable) | index into the text plus edits | yes | yes, in place | `json_editable_document`; keeps member order and number spellings |
| yyjson | immutable (`yyjson_read`) | yes | via a mutable copy | |
| simdjson DOM | immutable, parser reused | yes | no | "fresh" uses a new parser per parse |
| simdjson On-Demand | none: forward-only, lazy | no | no | only traverse and select |
+24 -92
View File
@@ -8,9 +8,8 @@
// Same-feature-set benchmark: read-only JSON documents with random access.
//
// json_view nlohmann/json_view.hpp (fresh document per parse / reused;
// "(editable)": json_editable_document, same read API)
// yyjson yyjson_read(): immutable document, random access
// json_view nlohmann/json_view.hpp (fresh document per parse / reused)
// yyjson yyjson_read(): immutable document, random access
// simdjson DOM dom::parser (reused, as recommended; "fresh": a new parser
// per parse): immutable, random access
// references (different feature sets):
@@ -20,11 +19,7 @@
//
// Workloads: parse (build + free), traverse (visit everything, convert every
// number, touch every string and key), select (a few fields per document),
// dump (compact serialization of the parsed document), and parse+dump (a fresh
// document, then its dump, in one timed call). json_view converts floating-point
// numbers lazily (when read or dumped), yyjson and simdjson DOM while parsing:
// for float-heavy documents, traverse (which includes the parse) and parse+dump
// are the fair reading of the parse and dump rows.
// dump (compact serialization of the parsed document).
// All engines run interleaved in every round, each timed call after an untimed
// one of the same engine; the best round is reported.
#include <nlohmann/json_view.hpp>
@@ -48,8 +43,6 @@
using nlohmann::json;
using nlohmann::json_document;
using nlohmann::json_editable_document;
using nlohmann::json_editable_view;
using nlohmann::json_view;
static volatile double g_sink;
@@ -62,8 +55,7 @@ struct stats
// ---------------- traversal ----------------
template<class V>
static void walk_view(V v, stats& st)
static void walk(json_view v, stats& st)
{
++st.nodes;
switch (v.type())
@@ -72,42 +64,32 @@ static void walk_view(V v, stats& st)
for (auto it = v.begin(); it != v.end(); ++it)
{
st.str += it.key().size();
walk_view<V>(*it, st);
walk(*it, st);
}
break;
case json::value_t::array:
for (const V e : v)
for (const json_view e : v)
{
walk_view<V>(e, st);
walk(e, st);
}
break;
case json::value_t::string:
st.str += v.get_string().size();
break;
case json::value_t::number_integer:
st.num += static_cast<double>(v.template get<std::int64_t>());
st.num += static_cast<double>(v.get<std::int64_t>());
break;
case json::value_t::number_unsigned:
st.num += static_cast<double>(v.template get<std::uint64_t>());
st.num += static_cast<double>(v.get<std::uint64_t>());
break;
case json::value_t::number_float:
st.num += v.template get<double>();
st.num += v.get<double>();
break;
default:
break;
}
}
static void walk(json_view v, stats& st)
{
walk_view<json_view>(v, st);
}
static void walk(json_editable_view v, stats& st)
{
walk_view<json_editable_view>(v, st);
}
static void walk(const json& j, stats& st)
{
++st.nodes;
@@ -318,21 +300,20 @@ static void walk(const boost::json::value& v, stats& st)
// status (one tweet): id, user.screen_name, retweet_count
// rpc: method, params.minuend, id
template<class V>
static double pick_view(const std::string& name, V r)
static double pick(const std::string& name, json_view r)
{
double acc = 0;
if (name == "twitter" || name == "status")
{
auto one = [&](V s)
auto one = [&](json_view s)
{
acc += static_cast<double>(s["id"].template get<std::uint64_t>());
acc += static_cast<double>(s["id"].get<std::uint64_t>());
acc += static_cast<double>(s["user"]["screen_name"].get_string().size());
acc += static_cast<double>(s["retweet_count"].template get<std::int64_t>());
acc += static_cast<double>(s["retweet_count"].get<std::int64_t>());
};
if (name == "twitter")
{
for (const V s : r["statuses"])
for (const json_view s : r["statuses"])
{
one(s);
}
@@ -344,20 +325,20 @@ static double pick_view(const std::string& name, V r)
}
else if (name == "citm_catalog")
{
for (const V p : r["performances"])
for (const json_view p : r["performances"])
{
acc += static_cast<double>(p["id"].template get<std::uint64_t>() + p["eventId"].template get<std::uint64_t>() + p["seatCategories"].size());
acc += static_cast<double>(p["id"].get<std::uint64_t>() + p["eventId"].get<std::uint64_t>() + p["seatCategories"].size());
}
acc += static_cast<double>(r["events"].size());
}
else if (name == "canada")
{
const V g = r["features"][0]["geometry"];
const json_view g = r["features"][0]["geometry"];
acc += static_cast<double>(r["type"].get_string().size() + g["type"].get_string().size() + g["coordinates"].size());
}
else if (name == "jeopardy")
{
for (const V q : r)
for (const json_view q : r)
{
acc += q["round"].get_string() == "Final Jeopardy!" ? 1 : 0;
acc += static_cast<double>(q["category"].get_string().size());
@@ -366,21 +347,11 @@ static double pick_view(const std::string& name, V r)
else if (name == "rpc")
{
acc += static_cast<double>(r["method"].get_string().size());
acc += static_cast<double>(r["params"]["minuend"].template get<std::int64_t>() + r["id"].template get<std::int64_t>());
acc += static_cast<double>(r["params"]["minuend"].get<std::int64_t>() + r["id"].get<std::int64_t>());
}
return acc;
}
static double pick(const std::string& name, json_view r)
{
return pick_view<json_view>(name, r);
}
static double pick(const std::string& name, json_editable_view r)
{
return pick_view<json_editable_view>(name, r);
}
static double pick(const std::string& name, const json& r)
{
double acc = 0;
@@ -663,35 +634,15 @@ int main(int argc, char** argv)
walk(json::parse(dc.text), a);
auto d = json_document::parse(dc.text);
walk(d.root(), b);
auto ed = json_editable_document::parse(dc.text);
stats e;
walk(ed.root(), e);
yyjson_doc* y = yyjson_read(dc.text.data(), dc.text.size(), 0);
walk(yyjson_doc_get_root(y), c);
simdjson::dom::parser p;
walk(p.parse(dc.text).value(), dd);
const bool ok = a.nodes == b.nodes && a.nodes == e.nodes && a.str == e.str && std::fabs(a.num - e.num) <= 1e-9 * std::fabs(a.num) && a.nodes == c.nodes && a.nodes == dd.nodes && a.str == b.str && a.str == c.str && a.str == dd.str
const bool ok = a.nodes == b.nodes && a.nodes == c.nodes && a.nodes == dd.nodes && a.str == b.str && a.str == c.str && a.str == dd.str
&& std::fabs(a.num - b.num) <= 1e-9 * std::fabs(a.num) && std::fabs(a.num - c.num) <= 1e-9 * std::fabs(a.num);
const double pa = pick(dc.name, json::parse(dc.text)), pb = pick(dc.name, d.root()), pe = pick(dc.name, ed.root()), pc = pick(dc.name, yyjson_doc_get_root(y)), pd = pick(dc.name, p.parse(dc.text).value());
std::printf("check %-13s traverse %s select %s\n", dc.name.c_str(), ok ? "OK" : "MISMATCH", (pa == pb && pa == pe && pa == pc && pa == pd) ? "OK" : "MISMATCH");
const double pa = pick(dc.name, json::parse(dc.text)), pb = pick(dc.name, d.root()), pc = pick(dc.name, yyjson_doc_get_root(y)), pd = pick(dc.name, p.parse(dc.text).value());
std::printf("check %-13s traverse %s select %s\n", dc.name.c_str(), ok ? "OK" : "MISMATCH", (pa == pb && pa == pc && pa == pd) ? "OK" : "MISMATCH");
yyjson_doc_free(y);
// the dumps of the engines may format floats differently, so only: every dump is non-empty, and the
// dump of json_view (also editable) and of json::parse parse back to the same number of nodes
const std::string dj = json::parse(dc.text).dump();
const std::string dv = d.root().dump();
const std::string de = ed.root().dump();
std::size_t yn = 0;
yyjson_doc* y2 = yyjson_read(dc.text.data(), dc.text.size(), 0);
char* yo = yyjson_write(y2, 0, &yn);
const bool dumps_ok = !dj.empty() && !dv.empty() && !de.empty() && yo != nullptr && yn > 0 && !simdjson::to_string(p.parse(dc.text).value()).empty();
std::free(yo);
yyjson_doc_free(y2);
stats sj_nodes, sv_nodes, se_nodes;
walk(json::parse(dj), sj_nodes);
walk(json::parse(dv), sv_nodes);
walk(json::parse(de), se_nodes);
std::printf("check %-13s dump %s\n", dc.name.c_str(), (dumps_ok && sj_nodes.nodes == a.nodes && sv_nodes.nodes == a.nodes && se_nodes.nodes == a.nodes) ? "OK" : "MISMATCH");
}
std::FILE* csv = std::fopen("bench_view.csv", "w");
@@ -718,7 +669,6 @@ int main(int argc, char** argv)
workloads.push_back({"parse", {
{"json_view", [&] { auto d = json_document::parse(s); g_sink = static_cast<double>(d.node_count()); }},
{"json_view (reused)", [&] { reused.read(s); g_sink = static_cast<double>(reused.node_count()); }},
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); g_sink = static_cast<double>(d.root().size()); }},
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = static_cast<double>(yyjson_doc_get_val_count(d)); yyjson_doc_free(d); }},
{"simdjson DOM", [&] { auto e = sj.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
{"simdjson DOM (fresh)", [&] { simdjson::dom::parser p; auto e = p.parse(ps).value_unsafe(); g_sink = e.is_object(); }},
@@ -730,7 +680,6 @@ int main(int argc, char** argv)
workloads.push_back({"traverse", {
{"json_view", [&] { auto d = json_document::parse(s); stats st; walk(d.root(), st); g_sink = st.num; }},
{"json_view (reused)", [&] { reused.read(s); stats st; walk(reused.root(), st); g_sink = st.num; }},
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); stats st; walk(d.root(), st); g_sink = st.num; }},
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); stats st; walk(yyjson_doc_get_root(d), st); g_sink = st.num; yyjson_doc_free(d); }},
{"simdjson DOM", [&] { stats st; walk(sj.parse(ps).value_unsafe(), st); g_sink = st.num; }},
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); stats st; walk_od(d.get_value().value_unsafe(), st); g_sink = st.num; }},
@@ -742,31 +691,15 @@ int main(int argc, char** argv)
workloads.push_back({"select", {
{"json_view", [&] { auto d = json_document::parse(s); g_sink = pick(name, d.root()); }},
{"json_view (reused)", [&] { reused.read(s); g_sink = pick(name, reused.root()); }},
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); g_sink = pick(name, d.root()); }},
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); g_sink = pick(name, yyjson_doc_get_root(d)); yyjson_doc_free(d); }},
{"simdjson DOM", [&] { g_sink = pick(name, sj.parse(ps).value_unsafe()); }},
{"simdjson OD", [&] { auto d = od.iterate(ps).value_unsafe(); g_sink = pick_od(name, d); }},
{"json::parse", [&] { json j = json::parse(s); g_sink = pick(name, j); }},
}});
// pipeline: a fresh document, then a dump of it, in one timed call
// (simdjson DOM reuses its parser, as in the parse row); the traverse
// workload above is already parse+traverse
workloads.push_back({"parse+dump", {
{"json_view", [&] { auto d = json_document::parse(s); std::string o = d.root().dump(); g_sink = static_cast<double>(o.size()); }},
{"json_view (editable)", [&] { auto d = json_editable_document::parse(s); std::string o = d.root().dump(); g_sink = static_cast<double>(o.size()); }},
{"yyjson", [&] { yyjson_doc* d = yyjson_read(s.data(), s.size(), 0); std::size_t n = 0; char* o = yyjson_write(d, 0, &n); g_sink = static_cast<double>(n); std::free(o); yyjson_doc_free(d); }},
{"simdjson DOM", [&] { std::string o = simdjson::to_string(sj.parse(ps).value_unsafe()); g_sink = static_cast<double>(o.size()); }},
#if JSON_VIEW_BENCH_BOOST
{"Boost.JSON", [&] { boost::json::monotonic_resource mr; auto v = boost::json::parse(s, &mr); std::string o = boost::json::serialize(v); g_sink = static_cast<double>(o.size()); }},
#endif
{"json::parse", [&] { json j = json::parse(s); std::string o = j.dump(); g_sink = static_cast<double>(o.size()); }},
}});
{
// serialization of an already parsed document
static json_document vd;
vd.read(s);
static json_editable_document ed = json_editable_document::parse(s);
ed = json_editable_document::parse(s);
static yyjson_doc* yd = nullptr;
if (yd)
{
@@ -780,7 +713,6 @@ int main(int argc, char** argv)
jd = json::parse(s);
workloads.push_back({"dump", {
{"json_view", [&] { std::string o = vd.root().dump(); g_sink = static_cast<double>(o.size()); }},
{"json_view (editable)", [&] { std::string o = ed.root().dump(); g_sink = static_cast<double>(o.size()); }},
{"yyjson", [&] { std::size_t n = 0; char* o = yyjson_write(yd, 0, &n); g_sink = static_cast<double>(n); std::free(o); }},
{"simdjson DOM", [&] { std::string o = simdjson::to_string(se); g_sink = static_cast<double>(o.size()); }},
{"json::parse", [&] { std::string o = jd.dump(); g_sink = static_cast<double>(o.size()); }},
@@ -808,7 +740,7 @@ int main(int argc, char** argv)
}
}
const double ref = best[0];
std::printf("%-13s %-14s", dc.name.c_str(), wl.first.c_str());
std::printf("%-13s %-9s", dc.name.c_str(), wl.first.c_str());
for (std::size_t k = 0; k < wl.second.size(); ++k)
{
const double us = best[k] / 1e3;
+25 -47
View File
@@ -239,8 +239,6 @@ def main():
ap.add_argument('--download', action='store_true', help='use pinned downloads instead of system libraries')
ap.add_argument('--no-boost', action='store_true', help='skip Boost.JSON')
ap.add_argument('--native', action='store_true', help='compile for this CPU (-march=native / -mcpu=native)')
ap.add_argument('--aligned', action='store_true',
help='also build and run with all functions (and loops) aligned, to make code layout artifacts visible')
ap.add_argument('--rounds', type=int, default=30, help='rounds of bench_view (default: 30)')
ap.add_argument('--corpus', nargs='*', default=[], help='more files for bench_corpus')
ap.add_argument('--build-dir', default=os.path.join(HERE, 'build'), help='where to build (default: build/ next to this script)')
@@ -284,46 +282,32 @@ def main():
+ ['-I' + d for d in lib.include])
objects.append(obj)
cxx_version = output(cxx + ['--version'])
is_clang = 'clang' in cxx_version.lower()
# alignment flags: functions and (gcc) loops on 64-byte boundaries
align_flags = ['-mllvm', '-align-all-functions=6'] if is_clang else ['-falign-functions=64', '-falign-loops=64']
# the builds: the default one and, with --aligned, one with aligned code
variants = [('default build', args.build_dir, [])]
if args.aligned:
variants.append(('aligned build', os.path.join(args.build_dir, 'aligned'), align_flags))
binaries = {}
for bench in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
exe = os.path.join(args.build_dir, bench)
run(cxx + flags + include + [os.path.join(HERE, bench + '.cpp')] + objects + link + ['-o', exe])
binaries[bench] = exe
# run: bench_view on its documents, bench_corpus on those and the given files
corpus = [os.path.join(args.data, f) for f in DEFAULT_CORPUS] + args.corpus
# label -> {benchmark: output}
all_outputs = {}
for label, build_dir, extra in variants:
os.makedirs(build_dir, exist_ok=True)
binaries = {}
for bench in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
exe = os.path.join(build_dir, bench)
run(cxx + flags + extra + include + [os.path.join(HERE, bench + '.cpp')] + objects + link + ['-o', exe])
binaries[bench] = exe
# run: bench_view on its documents, bench_corpus on those and the given files
outputs = {}
outputs['bench_view'] = run([binaries['bench_view'], args.data, str(args.rounds)], cwd=build_dir,
capture_output=True, text=True).stdout
outputs['bench_corpus'] = run([binaries['bench_corpus']] + corpus, cwd=build_dir,
capture_output=True, text=True).stdout
outputs['bench_edit'] = run([binaries['bench_edit'], args.data, str(max(1, args.rounds // 2))], cwd=build_dir,
capture_output=True, text=True).stdout
outputs['bench_decode'] = run([binaries['bench_decode'], args.data, str(max(1, args.rounds // 2))],
cwd=build_dir, capture_output=True, text=True).stdout
for name, text in outputs.items():
print(text)
all_outputs[label] = outputs
outputs = {}
outputs['bench_view'] = run([binaries['bench_view'], args.data, str(args.rounds)], cwd=args.build_dir,
capture_output=True, text=True).stdout
outputs['bench_corpus'] = run([binaries['bench_corpus']] + corpus, cwd=args.build_dir,
capture_output=True, text=True).stdout
outputs['bench_edit'] = run([binaries['bench_edit'], args.data, str(max(1, args.rounds // 2))], cwd=args.build_dir,
capture_output=True, text=True).stdout
outputs['bench_decode'] = run([binaries['bench_decode'], args.data, str(max(1, args.rounds // 2))], cwd=args.build_dir,
capture_output=True, text=True).stdout
for name, text in outputs.items():
print(text)
# results with their metadata
now = datetime.datetime.now()
host = re.sub(r'[^A-Za-z0-9-]+', '-', platform.node().split('.')[0]) or 'host'
stem = os.path.join(HERE, 'results', f'{now:%Y-%m-%d}-{host}')
os.makedirs(os.path.dirname(stem), exist_ok=True)
cxx_version = output(cxx + ['--version'])
compiler_version = cxx_version.splitlines()[0] if cxx_version else ' '.join(cxx)
meta = [
('date', f'{now:%Y-%m-%d %H:%M}'),
@@ -332,7 +316,6 @@ def main():
('OS', f'{platform.system()} {platform.release()} ({platform.machine()})'),
('compiler', compiler_version),
('flags', ' '.join(flags)),
('aligned results', ('included (flags: ' + ' '.join(align_flags) + ')') if args.aligned else 'not included'),
('yyjson', libs['yyjson'].version),
('simdjson', libs['simdjson'].version),
('Boost.JSON', libs['boost'].version if with_boost else 'skipped (not found)'),
@@ -345,20 +328,15 @@ def main():
f.write('| | |\n|---|---|\n')
for key, value in meta:
f.write(f'| {key} | {value} |\n')
for label, outputs in all_outputs.items():
# (without --aligned there is only the default build, and no extra heading level)
prefix = f'{label}: ' if args.aligned else ''
for name, text in outputs.items():
f.write(f'\n## {prefix}{name}\n\n```\n{text.rstrip()}\n```\n')
for name, text in outputs.items():
f.write(f'\n## {name}\n\n```\n{text.rstrip()}\n```\n')
with open(stem + '.csv', 'w', encoding='utf-8') as out:
out.write(''.join(f'# {key}: {value}\n' for key, value in meta))
for label, build_dir, _ in variants:
for name in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
path = os.path.join(build_dir, name + '.csv')
if os.path.isfile(path):
with open(path, encoding='utf-8') as f:
# (the default build keeps its plain section names)
out.write(f'# {name}' + (f' ({label})' if args.aligned else '') + '\n' + f.read())
for name in ['bench_view', 'bench_corpus', 'bench_edit', 'bench_decode']:
path = os.path.join(args.build_dir, name + '.csv')
if os.path.isfile(path):
with open(path, encoding='utf-8') as f:
out.write(f'# {name}\n' + f.read())
print(f'results: {stem}.md, {stem}.csv')
-21
View File
@@ -15,7 +15,6 @@
#include <nlohmann/json_view.hpp>
#include <algorithm> // any_of
#include <cstddef> // size_t
#include <functional> // function
#include <random> // mt19937
#include <string> // string
@@ -89,26 +88,6 @@ inline bool has_duplicate_keys(const nlohmann::ordered_json_view& v)
});
}
// the message of an exception about the value at path (a JSON pointer, "" for
// the root) whose source bytes are [start, end): the context basic_json adds
// with JSON_DIAGNOSTICS and JSON_DIAGNOSTIC_POSITIONS, if enabled, then what
inline std::string message(const std::string& id, const std::string& path, std::size_t start, std::size_t end, const std::string& what)
{
std::string context;
#if JSON_DIAGNOSTIC_POSITIONS
context = "(bytes " + std::to_string(start) + "-" + std::to_string(end) + ") ";
#else
static_cast<void>(start);
static_cast<void>(end);
#endif
#if JSON_DIAGNOSTICS
context = path.empty() ? "" : "(" + path + ") " + context;
#else
static_cast<void>(path);
#endif
return "[json.exception." + id + "] " + context + what;
}
#if !defined(JSON_NOEXCEPTION)
// the exception a call throws, or "" if it throws none
inline std::string exception_of_call(const std::function<void()>& f)
+71 -39
View File
@@ -225,7 +225,7 @@ TEST_CASE("json_view")
#if JSON_DIAGNOSTICS
// the parents are set, so errors name the path
const json m = materialized<json_document>(R"({"a":{"b":[1]}})");
CHECK_THROWS_WITH_AS(m.at("a").at("b").at(0).at("x"), json_view_test::message("type_error.304", "/a/b/0", 11, 12, "cannot use at() with number").c_str(), json::type_error&);
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
}
@@ -750,6 +750,19 @@ std::string exception_of(F f)
return "";
}
// the exception a call on a basic_json value throws, without the byte range of
// the value (JSON_DIAGNOSTIC_POSITIONS), which the exceptions of views lack
template<typename F>
std::string json_exception_of(F f)
{
std::string what = exception_of(f);
const std::size_t at = what.find("] (bytes ");
if (at != std::string::npos)
{
what.erase(at + 2, what.find(") ", at) - at);
}
return what;
}
#endif
// compares a view with the ordered_json value materialize() gives for it:
@@ -958,25 +971,24 @@ TEST_CASE("json_view element access and iteration")
#if !defined(JSON_NOEXCEPTION)
if (!j.is_object())
{
CHECK(exception_of([&] { static_cast<void>(v["a"]); }) == exception_of([&] { static_cast<void>(j["a"]); }));
CHECK(exception_of([&] { static_cast<void>(v["a"]); }) == json_exception_of([&] { static_cast<void>(j["a"]); }));
}
if (!j.is_array())
{
CHECK(exception_of([&] { static_cast<void>(v[0]); }) == exception_of([&] { static_cast<void>(j[0]); }));
CHECK(exception_of([&] { static_cast<void>(v[0]); }) == json_exception_of([&] { static_cast<void>(j[0]); }));
}
CHECK(exception_of([&] { static_cast<void>(v.at("a")); }) == exception_of([&] { static_cast<void>(j.at("a")); }));
CHECK(exception_of([&] { static_cast<void>(v.at("missing")); }) == exception_of([&] { static_cast<void>(j.at("missing")); }));
CHECK(exception_of([&] { static_cast<void>(v.at(0)); }) == exception_of([&] { static_cast<void>(j.at(0)); }));
CHECK(exception_of([&] { static_cast<void>(v.at(5)); }) == exception_of([&] { static_cast<void>(j.at(5)); }));
CHECK(exception_of([&] { static_cast<void>(v.at("a")); }) == json_exception_of([&] { static_cast<void>(j.at("a")); }));
CHECK(exception_of([&] { static_cast<void>(v.at("missing")); }) == json_exception_of([&] { static_cast<void>(j.at("missing")); }));
CHECK(exception_of([&] { static_cast<void>(v.at(0)); }) == json_exception_of([&] { static_cast<void>(j.at(0)); }));
CHECK(exception_of([&] { static_cast<void>(v.at(5)); }) == json_exception_of([&] { static_cast<void>(j.at(5)); }));
if (!(j.is_object() && j.empty())) // (key() of an end iterator)
{
// (without the context of basic_json: view iterators do not know the value they iterate over)
CHECK(exception_of([&] { static_cast<void>(v.begin().key()); }) == (j.is_object() ? "" : "[json.exception.invalid_iterator.207] cannot use key() for non-object iterators"));
CHECK(exception_of([&] { static_cast<void>(v.begin().key()); }) == json_exception_of([&] { static_cast<void>(j.begin().key()); }));
}
if (!j.empty() || j.is_null())
{
CHECK(exception_of([&] { static_cast<void>(v.front()); }) == exception_of([&] { static_cast<void>(j.front()); }));
CHECK(exception_of([&] { static_cast<void>(v.back()); }) == exception_of([&] { static_cast<void>(j.back()); }));
CHECK(exception_of([&] { static_cast<void>(v.front()); }) == json_exception_of([&] { static_cast<void>(j.front()); }));
CHECK(exception_of([&] { static_cast<void>(v.back()); }) == json_exception_of([&] { static_cast<void>(j.back()); }));
}
#endif
CHECK(v.contains("a") == j.contains("a"));
@@ -988,8 +1000,8 @@ TEST_CASE("json_view element access and iteration")
const json_document d = json_document::parse(R"({"a":[]})");
CHECK(d.root()["b"].is_discarded());
CHECK(d.root()["a"][0].is_discarded());
CHECK_THROWS_WITH_AS(d.root()["a"].front(), json_view_test::message("invalid_iterator.214", "/a", 5, 7, "cannot get value").c_str(), json::invalid_iterator&);
CHECK_THROWS_WITH_AS(d.root()["a"].back(), json_view_test::message("invalid_iterator.214", "/a", 5, 7, "cannot get value").c_str(), json::invalid_iterator&);
CHECK_THROWS_WITH_AS(d.root()["a"].front(), "[json.exception.invalid_iterator.214] cannot get value", json::invalid_iterator&);
CHECK_THROWS_WITH_AS(d.root()["a"].back(), "[json.exception.invalid_iterator.214] cannot get value", json::invalid_iterator&);
const json_view invalid{};
CHECK(invalid.begin() == invalid.end());
CHECK(std::string(invalid.type_name()) == "discarded");
@@ -1020,10 +1032,10 @@ TEST_CASE("json_view element access and iteration")
CHECK(v[json_view::json_pointer("/a/b/5")][0].is_discarded());
#if !defined(JSON_NOEXCEPTION)
// type errors on values that are not discarded stay
CHECK_THROWS_WITH_AS(v[0], json_view_test::message("type_error.305", "", 0, 29, "cannot use operator[] with a numeric argument with object").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(v["a"]["b"]["c"], json_view_test::message("type_error.305", "/a/b", 10, 17, "cannot use operator[] with a string argument with array").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(v["s"]["c"], json_view_test::message("type_error.305", "/s", 23, 28, "cannot use operator[] with a string argument with string").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(v["s"][0], json_view_test::message("type_error.305", "/s", 23, 28, "cannot use operator[] with a numeric argument with string").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(v[0], "[json.exception.type_error.305] cannot use operator[] with a numeric argument with object", json::type_error&);
CHECK_THROWS_WITH_AS(v["a"]["b"]["c"], "[json.exception.type_error.305] cannot use operator[] with a string argument with array", json::type_error&);
CHECK_THROWS_WITH_AS(v["s"]["c"], "[json.exception.type_error.305] cannot use operator[] with a string argument with string", json::type_error&);
CHECK_THROWS_WITH_AS(v["s"][0], "[json.exception.type_error.305] cannot use operator[] with a numeric argument with string", json::type_error&);
CHECK_THROWS_AS(v["a"]["b"][0]["c"], json::type_error&);
CHECK_THROWS_AS(v["s"][json_view::json_pointer("/x")], json::out_of_range&);
// at() keeps throwing on a discarded view
@@ -1098,9 +1110,9 @@ TEST_CASE("json_view element access and iteration")
CHECK(v[(std::numeric_limits<std::size_t>::max)()].is_discarded());
CHECK(v[std::numeric_limits<int>::max()].is_discarded());
#if !defined(JSON_NOEXCEPTION)
CHECK_THROWS_WITH_AS(v.at(3), json_view_test::message("out_of_range.401", "", 0, 10, "array index 3 is out of range").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(v.at(3u), json_view_test::message("out_of_range.401", "", 0, 10, "array index 3 is out of range").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(v.at(std::int64_t(3)), json_view_test::message("out_of_range.401", "", 0, 10, "array index 3 is out of range").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(v.at(3), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
CHECK_THROWS_WITH_AS(v.at(3u), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
CHECK_THROWS_WITH_AS(v.at(std::int64_t(3)), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
CHECK_THROWS_AS(v.at(-1), json::out_of_range&);
CHECK_THROWS_AS(v.at(-1L), json::out_of_range&);
CHECK_THROWS_AS(v.at(std::int64_t(-1)), json::out_of_range&);
@@ -1234,6 +1246,26 @@ TEST_CASE("json_view element access and iteration")
namespace
{
#if !defined(JSON_NOEXCEPTION)
// an exception message without the context that basic_json adds with
// JSON_DIAGNOSTICS ("(/path) ") and JSON_DIAGNOSTIC_POSITIONS ("(bytes 1-2) ");
// the view's exceptions have no such context
std::string without_path(std::string msg)
{
for (const char* prefix :
{"] (/", "] (bytes "
})
{
const std::size_t open = msg.find(prefix);
if (open != std::string::npos)
{
msg.erase(open + 2, msg.find(") ", open) + 2 - (open + 2));
}
}
return msg;
}
#endif
// the bits of a float, to compare values bit for bit
std::uint64_t bits(double x)
{
@@ -1304,18 +1336,18 @@ void check_values(const ordered_json_view& v, const ordered_json& j, const std::
// conversions to the wrong type throw what basic_json throws
if (!j.is_number())
{
CHECK(exception_of([&] { static_cast<void>(v.get<int>()); }) == exception_of([&] { static_cast<void>(j.get<int>()); }));
CHECK(exception_of([&] { static_cast<void>(v.get<int>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<int>()); })));
}
CHECK(exception_of([&] { static_cast<void>(v.get<bool>()); }) == exception_of([&] { static_cast<void>(j.get<bool>()); }));
CHECK(exception_of([&] { static_cast<void>(v.get<std::string>()); }) == exception_of([&] { static_cast<void>(j.get<std::string>()); }));
CHECK(exception_of([&] { static_cast<void>(v.get<std::nullptr_t>()); }) == exception_of([&] { static_cast<void>(j.get<std::nullptr_t>()); }));
CHECK(exception_of([&] { static_cast<void>(v.get<bool>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<bool>()); })));
CHECK(exception_of([&] { static_cast<void>(v.get<std::string>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::string>()); })));
CHECK(exception_of([&] { static_cast<void>(v.get<std::nullptr_t>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::nullptr_t>()); })));
if (!j.is_array())
{
CHECK(exception_of([&] { static_cast<void>(v.get<std::vector<int>>()); }) == exception_of([&] { static_cast<void>(j.get<std::vector<int>>()); }));
CHECK(exception_of([&] { static_cast<void>(v.get<std::vector<int>>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::vector<int>>()); })));
}
if (!j.is_object())
{
CHECK(exception_of([&] { static_cast<void>(v.get<std::map<std::string, int>>()); }) == exception_of([&] { static_cast<void>(j.get<std::map<std::string, int>>()); }));
CHECK(exception_of([&] { static_cast<void>(v.get<std::map<std::string, int>>()); }) == without_path(exception_of([&] { static_cast<void>(j.get<std::map<std::string, int>>()); })));
}
#endif
@@ -1458,8 +1490,8 @@ TEST_CASE("json_view values")
CHECK(v[3].number_token() == "123456789012345678901234567890");
CHECK(v[4].number_token() == "-12");
CHECK(v[5].number_token() == "7");
CHECK_THROWS_WITH_AS(v[6].number_token(), json_view_test::message("type_error.302", "/6", 56, 59, "type must be number, but is string").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(v.get_string(), json_view_test::message("type_error.302", "", 0, 60, "type must be string, but is array").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(v[6].number_token(), "[json.exception.type_error.302] type must be number, but is string", json::type_error&);
CHECK_THROWS_WITH_AS(v.get_string(), "[json.exception.type_error.302] type must be string, but is array", json::type_error&);
}
SECTION("conversions")
@@ -1533,15 +1565,15 @@ TEST_CASE("json_view values")
CHECK(v["o"]["x"].value(json::json_pointer("/1"), 0) == j["o"]["x"].value(json::json_pointer("/1"), 0));
CHECK(v["o"]["x"].value(json::json_pointer("/7"), 3) == j["o"]["x"].value(json::json_pointer("/7"), 3));
#if !defined(JSON_NOEXCEPTION)
CHECK(exception_of([&] { static_cast<void>(v["o"]["x"].value("k", 0)); }) == exception_of([&] { static_cast<void>(j["o"]["x"].value("k", 0)); }));
CHECK(exception_of([&] { static_cast<void>(v.value("s", 0)); }) == exception_of([&] { static_cast<void>(j.value("s", 0)); }));
CHECK(exception_of([&] { static_cast<void>(v["n"].value("x", 0)); }) == exception_of([&] { static_cast<void>(j["n"].value("x", 0)); }));
CHECK(exception_of([&] { static_cast<void>(v["n"].value(json::json_pointer("/x"), 0)); }) == exception_of([&] { static_cast<void>(j["n"].value(json::json_pointer("/x"), 0)); }));
CHECK(exception_of([&] { static_cast<void>(v["o"]["x"].value("k", 0)); }) == without_path(exception_of([&] { static_cast<void>(j["o"]["x"].value("k", 0)); })));
CHECK(exception_of([&] { static_cast<void>(v.value("s", 0)); }) == without_path(exception_of([&] { static_cast<void>(j.value("s", 0)); })));
CHECK(exception_of([&] { static_cast<void>(v["n"].value("x", 0)); }) == without_path(exception_of([&] { static_cast<void>(j["n"].value("x", 0)); })));
CHECK(exception_of([&] { static_cast<void>(v["n"].value(json::json_pointer("/x"), 0)); }) == without_path(exception_of([&] { static_cast<void>(j["n"].value(json::json_pointer("/x"), 0)); })));
// string defaults: the member is not a string, or the value is neither an object nor an array
CHECK(exception_of([&] { static_cast<void>(v.value("n", "default")); }) == exception_of([&] { static_cast<void>(j.value("n", "default")); }));
CHECK(exception_of([&] { static_cast<void>(v["n"].value("x", "default")); }) == exception_of([&] { static_cast<void>(j["n"].value("x", "default")); }));
CHECK(exception_of([&] { static_cast<void>(v.value(json::json_pointer("/n"), "default")); }) == exception_of([&] { static_cast<void>(j.value(json::json_pointer("/n"), "default")); }));
CHECK(exception_of([&] { static_cast<void>(v["s"].value(json::json_pointer("/x"), "default")); }) == exception_of([&] { static_cast<void>(j["s"].value(json::json_pointer("/x"), "default")); }));
CHECK(exception_of([&] { static_cast<void>(v.value("n", "default")); }) == without_path(exception_of([&] { static_cast<void>(j.value("n", "default")); })));
CHECK(exception_of([&] { static_cast<void>(v["n"].value("x", "default")); }) == without_path(exception_of([&] { static_cast<void>(j["n"].value("x", "default")); })));
CHECK(exception_of([&] { static_cast<void>(v.value(json::json_pointer("/n"), "default")); }) == without_path(exception_of([&] { static_cast<void>(j.value(json::json_pointer("/n"), "default")); })));
CHECK(exception_of([&] { static_cast<void>(v["s"].value(json::json_pointer("/x"), "default")); }) == without_path(exception_of([&] { static_cast<void>(j["s"].value(json::json_pointer("/x"), "default")); })));
#endif
// (defaults and members longer than the short string optimization)
CHECK(v.value("s", "a default longer than fifteen characters") == "text");
@@ -1600,7 +1632,7 @@ TEST_CASE("json_view JSON pointers")
{
CAPTURE(pointer)
const json::json_pointer p(pointer);
const std::string at_error = exception_of([&] { static_cast<void>(j.at(p)); });
const std::string at_error = without_path(exception_of([&] { static_cast<void>(j.at(p)); }));
CHECK(exception_of([&] { static_cast<void>(v.at(p)); }) == at_error);
if (at_error.empty())
{
@@ -1614,7 +1646,7 @@ TEST_CASE("json_view JSON pointers")
}
else
{
CHECK(exception_of([&] { static_cast<void>(v[p]); }) == exception_of([&] { static_cast<void>(j[p]); }));
CHECK(exception_of([&] { static_cast<void>(v[p]); }) == without_path(exception_of([&] { static_cast<void>(j[p]); })));
}
// (basic_json::contains() throws out_of_range.404 for an empty
// array index token, although it is not meant to throw; the view
@@ -1625,7 +1657,7 @@ TEST_CASE("json_view JSON pointers")
static_cast<void>(found);
});
CHECK(v.contains(p) == (contains_error.empty() && j.contains(p)));
CHECK(exception_of([&] { static_cast<void>(v.value(p, 5)); }) == exception_of([&] { static_cast<void>(j.value(p, 5)); }));
CHECK(exception_of([&] { static_cast<void>(v.value(p, 5)); }) == without_path(exception_of([&] { static_cast<void>(j.value(p, 5)); })));
}
}
#endif
-364
View File
@@ -1,364 +0,0 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
#include <nlohmann/json_view.hpp>
using nlohmann::json;
using nlohmann::ordered_json;
using nlohmann::json_document;
using nlohmann::ordered_json_editable_document;
using nlohmann::json_view;
using nlohmann::ordered_json_document;
#include "json_view_test_helpers.hpp"
using json_view_test::generator;
#include <array>
#include <cstddef>
#include <string>
#include <vector>
// These tests were split off unit-json_view_dump.cpp, whose object file got too
// large for the MinGW assembler ("file too big"). Like that file, this one is
// also built with C++17, as it mentions JSON_HAS_CPP_17.
TEST_CASE("json_view comparison")
{
SECTION("equality of the values parse() produces")
{
generator g;
std::vector<std::string> texts;
for (int i = 0; i < 600; ++i)
{
std::string text;
g.value(text, 0);
texts.push_back(text);
// the same value written differently: sorted keys, canonical numbers
texts.push_back(json::parse(text).dump(1));
}
for (std::size_t i = 0; i + 2 < texts.size(); ++i)
{
for (std::size_t k = i; k < i + 3; ++k)
{
CAPTURE(texts[i])
CAPTURE(texts[k])
const json_document a = json_document::parse(texts[i]);
const json_document b = json_document::parse(texts[k]);
const json ja = json::parse(texts[i]);
const json jb = json::parse(texts[k]);
CHECK((a.root() == b.root()) == (ja == jb));
CHECK((a.root() != b.root()) == (ja != jb));
CHECK((a.root() == jb) == (ja == jb));
CHECK((jb == a.root()) == (ja == jb));
CHECK((a.root() != jb) == (ja != jb));
CHECK((jb != a.root()) == (ja != jb));
// ordered_json compares members in order
const ordered_json_document oa = ordered_json_document::parse(texts[i]);
const ordered_json_document ob = ordered_json_document::parse(texts[k]);
const ordered_json oja = ordered_json::parse(texts[i]);
const ordered_json ojb = ordered_json::parse(texts[k]);
CHECK((oa.root() == ob.root()) == (oja == ojb));
CHECK((oa.root() == ojb) == (oja == ojb));
}
}
}
SECTION("objects whose first keys differ or repeat")
{
const std::array<const char*, 17> texts =
{
{
R"({"a":1,"b":2})", R"({"a":1,"c":2})", R"({"b":1,"a":2})", R"({"a":1,"a":2,"b":3})", R"({"a":2,"b":3})",
R"({"a":1,"b":1,"a":2})", R"({"a":2,"b":1})", R"({"a":1,"a":2})", R"({"b":1})", R"({"a":1,"b":2,"c":3,"d":4,"e":5})",
R"({"a":1,"b":2,"c":3,"d":4,"f":5})", R"({"a":1,"b":2,"c":3,"a":4,"e":5})", R"({"a":4,"b":2,"c":3,"e":5})",
R"({"\u0061":1,"b":2})", R"({})", R"([{"a":1,"b":2}])", R"([{"a":1,"c":2}])"
}
};
for (const char* const x : texts)
{
for (const char* const y : texts)
{
CAPTURE(x)
CAPTURE(y)
const ordered_json_document dx = ordered_json_document::parse(x);
const ordered_json_document dy = ordered_json_document::parse(y);
const ordered_json_editable_document ex = ordered_json_editable_document::parse(x);
const ordered_json jx = ordered_json::parse(x);
const ordered_json jy = ordered_json::parse(y);
CHECK((dx.root() == dy.root()) == (jx == jy));
CHECK((dx.root() == jy) == (jx == jy));
CHECK((ex.root() == dy.root()) == (jx == jy));
}
}
}
SECTION("objects with duplicate keys, small and large")
{
// n members with keys in descending order; with duplicates, some keys
// repeat later (the first after unique ones in key order); in reverse,
// the members are in ascending key order
const auto object = [](int n, bool duplicates, bool reverse, int value)
{
std::string s = "{";
for (int i = 0; i < n; ++i)
{
const int k = reverse ? i : n - 1 - i;
s += (i != 0 ? ",\"k" : "\"k") + std::to_string(100 + k) + "\":" + std::to_string(value + k);
}
if (duplicates)
{
for (int k = n / 2; k < n; k += 3)
{
s += ",\"k" + std::to_string(100 + k) + "\":" + std::to_string(value + k);
}
}
return s + "}";
};
std::vector<std::string> texts;
for (const int n :
{
3, 16, 17, 40
})
{
for (const int flags :
{
0, 1, 2, 3
})
{
texts.push_back(object(n, (flags & 1) != 0, (flags & 2) != 0, 0));
}
texts.push_back(object(n, true, false, 1)); // other values
}
// many objects in one comparison (working memory reused across objects)
std::string array = "[";
std::string array_other = "[";
for (std::size_t i = 0; i < texts.size(); ++i)
{
array += (i != 0 ? "," : "") + texts[i];
array_other += (i != 0 ? "," : "") + texts[texts.size() - 1 - i];
}
texts.push_back(array + "]");
texts.push_back(array_other + "]");
for (const std::string& x : texts)
{
for (const std::string& y : texts)
{
CAPTURE(x)
CAPTURE(y)
const ordered_json_document dx = ordered_json_document::parse(x);
const ordered_json_document dy = ordered_json_document::parse(y);
const ordered_json jx = ordered_json::parse(x);
const ordered_json jy = ordered_json::parse(y);
CHECK((dx.root() == dy.root()) == (jx == jy));
CHECK((dx.root() == jy) == (jx == jy));
const json_document ux = json_document::parse(x);
const json uy = json::parse(y);
CHECK((ux.root() == uy) == (json::parse(x) == uy));
}
}
}
SECTION("numbers, duplicate keys, member order")
{
const auto same = [](const char* x, const char* y)
{
const json_document dx = json_document::parse(x);
const json_document dy = json_document::parse(y);
return dx.root() == dy.root();
};
CHECK(same("1", "1.0"));
CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]"));
CHECK(!same("1", "1.5"));
CHECK(same("18446744073709551615", "18446744073709551615"));
CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})"));
CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})"));
CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})"));
CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})"));
CHECK(!same("[1, 2]", "[2, 1]"));
CHECK(!same("\"a\"", "\"b\""));
CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\""));
CHECK(!same("null", "false"));
CHECK(!same("[]", "{}"));
const ordered_json_document dup = ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})");
const ordered_json_document last = ordered_json_document::parse(R"({"a": 3, "b": 2})");
CHECK(dup.root() == last.root());
// (== compares as parse() resolves duplicates, a lookup finds the first)
CHECK(dup.root()["a"].materialize() == 1);
CHECK(dup.root()["a"] != last.root()["a"]);
const ordered_json_document ab = ordered_json_document::parse(R"({"a": 1, "b": 2})");
const ordered_json_document ba = ordered_json_document::parse(R"({"b": 2, "a": 1})");
CHECK(ab.root() != ba.root());
// discarded values compare as basic_json's do
const json discarded(json::value_t::discarded);
CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested
CHECK((json_view() == discarded) == (discarded == discarded)); // NOLINT(readability-container-size-empty)
const json_document null_document = json_document::parse("null");
CHECK(!(json_view() == null_document.root())); // NOLINT(readability-container-size-empty)
CHECK(!(null_document.root() == discarded));
}
SECTION("values that are the same text, or the same value written differently, at any depth")
{
// nested arrays and objects around depths where the walk over equal text gives up
for (const int depth :
{
1, 5, 47, 48, 49, 50, 60, 300
})
{
CAPTURE(depth)
std::string head;
std::string tail;
for (int i = 0; i < depth; ++i)
{
head += i % 2 == 0 ? R"({"k":[1,"s",{"x":true},)" : R"([null,2.5,{"y":)";
tail.insert(0, i % 2 == 0 ? "]}" : "}]");
}
// the same text, one number changed, and one number written differently
const auto with_number = [&](const char* number)
{
std::string result = head;
result += number;
result += tail;
return result;
};
const std::string text = with_number("7");
const std::string changed = with_number("8");
const std::string spelled = with_number("7.0");
const json_document a = json_document::parse(text);
const json_document b = json_document::parse(text);
const json_document c = json_document::parse(changed);
const json_document d = json_document::parse(spelled);
CHECK(a.root() == b.root());
CHECK(a.root() != c.root());
CHECK(c.root() != a.root());
CHECK(a.root() == d.root());
CHECK(d.root() == a.root());
CHECK(a.root() == json::parse(text));
CHECK(a.root() != json::parse(changed));
CHECK(json::parse(spelled) == a.root());
const nlohmann::json_editable_document e = nlohmann::json_editable_document::parse(text);
CHECK(a.root() == e.root());
CHECK(e.root() == a.root());
CHECK(c.root() != e.root());
CHECK(e.root() == e.root());
const ordered_json_document oa = ordered_json_document::parse(text);
const ordered_json_document oc = ordered_json_document::parse(changed);
CHECK(oa.root() != oc.root());
CHECK(oa.root() == ordered_json::parse(text));
CHECK(oc.root() != ordered_json::parse(text));
}
// not the same text, but the same value
const auto same = [](const char* x, const char* y)
{
const json_document dx = json_document::parse(x);
const json_document dy = json_document::parse(y);
const bool equal = dx.root() == dy.root();
CHECK(equal == (json::parse(x) == json::parse(y)));
CHECK(equal == (dx.root() == json::parse(y)));
return equal;
};
CHECK(same(R"({"a":1,"a":2})", R"({"a":3,"a":2})"));
CHECK(same(R"({"a":1,"b":[1,{"c":2,"c":3}]})", R"({"b":[1,{"c":9,"c":3}],"a":1})"));
CHECK(!same(R"({"a":1,"a":2})", R"({"a":2,"a":3})"));
CHECK(same("[1.0,-0,0,1e2]", "[1.00,0,-0,100.0]"));
CHECK(same(R"(["\u00e9","\n"])", "[\"\xc3\xa9\",\"\\n\"]"));
CHECK(!same(R"({"a":[1,2,{"b":null}]})", R"({"a":[1,2,{"b":false}]})"));
CHECK(!same(R"({"a":[1,2,{"b":null}]})", R"({"a":[1,2,{"c":null}]})"));
CHECK(!same(R"({"a":[1,2,{"b":null}]})", R"({"a":[1,2,{"b":null,"c":1}]})"));
// edited documents: moved sequences, links, and values of new nodes
nlohmann::json_editable_document edited = nlohmann::json_editable_document::parse(R"({"a":[1,2,3],"b":{"c":"d"},"e":0.5})");
const json_document plain = json_document::parse(R"({"a":[1,2,3,4],"b":{"c":"x","y":null},"e":0.5,"z":[]})");
edited.push_back(edited.root()["a"], 4);
edited.set(edited.root()["b"], "c", "x");
edited.set(edited.root()["b"], "y", nullptr);
edited.set(edited.root(), "z", json::array());
CHECK(edited.root() == plain.root());
CHECK(plain.root() == edited.root());
edited.erase(edited.root()["a"], 0);
CHECK(edited.root() != plain.root());
CHECK(edited.root()["a"] == json::parse("[2,3,4]"));
const json_document tail = json_document::parse("[2,3,4]");
CHECK(edited.root()["a"] == tail.root());
}
SECTION("large documents, with a difference at any place")
{
const auto make = [](std::size_t count, std::size_t changed, bool swap_members)
{
std::string text = R"({"b":1,"list":[)";
for (std::size_t i = 0; i < count; ++i)
{
text += (i != 0 ? "," : "");
const std::string id = std::to_string(i == changed ? i + 1 : i);
if (swap_members && i % 3 == 0)
{
text += R"({"v":[1.5,"s",{"k":)";
text += id;
text += R"(}],"id":)";
text += id;
text += "}";
}
else
{
text += R"({"id":)";
text += id;
text += R"(,"v":[1.5,"s",{"k":)";
text += id;
text += "}]}";
}
}
return text + R"(],"a":{"y":2,"x":3}})";
};
const std::size_t count = 3000;
for (const std::size_t changed :
{
count, std::size_t{0}, std::size_t{1}, count / 2, count - 1
})
{
for (const bool swap :
{
false, true
})
{
CAPTURE(changed)
CAPTURE(swap)
const std::string text_a = make(count, count, false);
const std::string text_b = make(count, changed, swap);
const json_document a = json_document::parse(text_a);
const json_document b = json_document::parse(text_b);
const bool equal = changed == count;
CHECK((a.root() == b.root()) == equal);
CHECK((b.root() == a.root()) == equal);
CHECK((a.root() == json::parse(text_b)) == equal);
CHECK((json::parse(text_b) == a.root()) == equal);
CHECK((a.root()["list"] == b.root()["list"]) == equal);
const ordered_json_document oa = ordered_json_document::parse(text_a);
const ordered_json_document ob = ordered_json_document::parse(text_b);
CHECK((oa.root() == ob.root()) == (equal && !swap));
CHECK((oa.root() == ordered_json::parse(text_b)) == (equal && !swap));
}
}
}
SECTION("deep nesting")
{
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
const json_document a = json_document::parse(deep);
const json_document b = json_document::parse(deep);
CHECK(a.root() == b.root());
CHECK(a.root() == json::parse(deep));
const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']');
const json_document c = json_document::parse(other);
CHECK(a.root() != c.root());
}
}
+96 -1
View File
@@ -432,6 +432,101 @@ TEST_CASE("json_view dump")
}
}
TEST_CASE("json_view comparison")
{
SECTION("equality of the values parse() produces")
{
generator g;
std::vector<std::string> texts;
for (int i = 0; i < 600; ++i)
{
std::string text;
g.value(text, 0);
texts.push_back(text);
// the same value written differently: sorted keys, canonical numbers
texts.push_back(json::parse(text).dump(1));
}
for (std::size_t i = 0; i + 2 < texts.size(); ++i)
{
for (std::size_t k = i; k < i + 3; ++k)
{
CAPTURE(texts[i])
CAPTURE(texts[k])
const json_document a = json_document::parse(texts[i]);
const json_document b = json_document::parse(texts[k]);
const json ja = json::parse(texts[i]);
const json jb = json::parse(texts[k]);
CHECK((a.root() == b.root()) == (ja == jb));
CHECK((a.root() != b.root()) == (ja != jb));
CHECK((a.root() == jb) == (ja == jb));
CHECK((jb == a.root()) == (ja == jb));
CHECK((a.root() != jb) == (ja != jb));
CHECK((jb != a.root()) == (ja != jb));
// ordered_json compares members in order
const ordered_json_document oa = ordered_json_document::parse(texts[i]);
const ordered_json_document ob = ordered_json_document::parse(texts[k]);
const ordered_json oja = ordered_json::parse(texts[i]);
const ordered_json ojb = ordered_json::parse(texts[k]);
CHECK((oa.root() == ob.root()) == (oja == ojb));
CHECK((oa.root() == ojb) == (oja == ojb));
}
}
}
SECTION("numbers, duplicate keys, member order")
{
const auto same = [](const char* x, const char* y)
{
const json_document dx = json_document::parse(x);
const json_document dy = json_document::parse(y);
return dx.root() == dy.root();
};
CHECK(same("1", "1.0"));
CHECK(same("[1, -1, 2.5]", "[1.0, -1.0, 25e-1]"));
CHECK(!same("1", "1.5"));
CHECK(same("18446744073709551615", "18446744073709551615"));
CHECK(same(R"({"a": 1, "a": 2})", R"({"a": 2})"));
CHECK(!same(R"({"a": 1, "a": 2})", R"({"a": 1})"));
CHECK(same(R"({"a": 1, "b": 2})", R"({"b": 2, "a": 1})"));
CHECK(!same(R"({"a": 1})", R"({"a": 1, "b": 2})"));
CHECK(!same("[1, 2]", "[2, 1]"));
CHECK(!same("\"a\"", "\"b\""));
CHECK(same("\"\\u00e9\"", "\"\xc3\xa9\""));
CHECK(!same("null", "false"));
CHECK(!same("[]", "{}"));
const ordered_json_document dup = ordered_json_document::parse(R"({"a": 1, "b": 2, "a": 3})");
const ordered_json_document last = ordered_json_document::parse(R"({"a": 3, "b": 2})");
CHECK(dup.root() == last.root());
// (== compares as parse() resolves duplicates, a lookup finds the first)
CHECK(dup.root()["a"].materialize() == 1);
CHECK(dup.root()["a"] != last.root()["a"]);
const ordered_json_document ab = ordered_json_document::parse(R"({"a": 1, "b": 2})");
const ordered_json_document ba = ordered_json_document::parse(R"({"b": 2, "a": 1})");
CHECK(ab.root() != ba.root());
// discarded values compare as basic_json's do
const json discarded(json::value_t::discarded);
CHECK((json_view() == json_view()) == (discarded == discarded)); // NOLINT(readability-container-size-empty): operator== is tested
CHECK((json_view() == discarded) == (discarded == discarded)); // NOLINT(readability-container-size-empty)
const json_document null_document = json_document::parse("null");
CHECK(!(json_view() == null_document.root())); // NOLINT(readability-container-size-empty)
CHECK(!(null_document.root() == discarded));
}
SECTION("deep nesting")
{
const std::string deep = std::string(100000, '[') + std::string(100000, ']');
const json_document a = json_document::parse(deep);
const json_document b = json_document::parse(deep);
CHECK(a.root() == b.root());
CHECK(a.root() == json::parse(deep));
const std::string other = std::string(100000, '[') + "1" + std::string(100000, ']');
const json_document c = json_document::parse(other);
CHECK(a.root() != c.root());
}
}
TEST_CASE("json_view large objects")
{
// objects with 128 members or more are looked up with a hash index
@@ -462,7 +557,7 @@ TEST_CASE("json_view large objects")
CHECK(v[""].get_string() == "empty key");
CHECK(v["k1"].get<int>() == 1); // the first of duplicate keys, as for small objects
CHECK(!v.contains("missing"));
CHECK_THROWS_WITH_AS(v.at("missing"), json_view_test::message("out_of_range.403", "", 0, text.size(), "key 'missing' not found").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(v.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&);
CHECK(v == j);
CHECK(v.materialize() == j);
}
+11 -131
View File
@@ -532,18 +532,18 @@ TEST_CASE("json_view edits: errors")
CHECK_THROWS_WITH_AS(d.set(other_editable.root(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
CHECK_THROWS_WITH_AS(d.set(json_editable_view(), 1), "[json.exception.invalid_iterator.202] view does not belong to this document", json::invalid_iterator&);
CHECK_THROWS_WITH_AS(d.set(root["a"], "k", 1), json_view_test::message("type_error.305", "/a", 21, 27, "cannot use operator[] with a string argument with array").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(d.set(root["o"], 0, 1), json_view_test::message("type_error.305", "/o", 6, 14, "cannot use operator[] with a numeric argument with object").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(d.set(root["a"], 2, 1), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index 2 is out of range").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(d.set(root["a"], -1, 1), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index -1 is out of range").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(d.push_back(root["o"], 1), json_view_test::message("type_error.308", "/o", 6, 14, "cannot use push_back() with object").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(d.insert(root["n"], 0, 1), json_view_test::message("type_error.309", "/n", 34, 35, "cannot use insert() with number").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(d.insert(root["a"], 3, 1), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index 3 is out of range").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(d.erase(root["n"], "k"), json_view_test::message("type_error.307", "/n", 34, 35, "cannot use erase() with number").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(d.erase(root["o"], 0), json_view_test::message("type_error.307", "/o", 6, 14, "cannot use erase() with object").c_str(), json::type_error&);
CHECK_THROWS_WITH_AS(d.erase(root["a"], 2), json_view_test::message("out_of_range.401", "/a", 21, 27, "array index 2 is out of range").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(d.set(root["a"], "k", 1), "[json.exception.type_error.305] cannot use operator[] with a string argument with array", json::type_error&);
CHECK_THROWS_WITH_AS(d.set(root["o"], 0, 1), "[json.exception.type_error.305] cannot use operator[] with a numeric argument with object", json::type_error&);
CHECK_THROWS_WITH_AS(d.set(root["a"], 2, 1), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.set(root["a"], -1, 1), "[json.exception.out_of_range.401] array index -1 is out of range", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.push_back(root["o"], 1), "[json.exception.type_error.308] cannot use push_back() with object", json::type_error&);
CHECK_THROWS_WITH_AS(d.insert(root["n"], 0, 1), "[json.exception.type_error.309] cannot use insert() with number", json::type_error&);
CHECK_THROWS_WITH_AS(d.insert(root["a"], 3, 1), "[json.exception.out_of_range.401] array index 3 is out of range", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.erase(root["n"], "k"), "[json.exception.type_error.307] cannot use erase() with number", json::type_error&);
CHECK_THROWS_WITH_AS(d.erase(root["o"], 0), "[json.exception.type_error.307] cannot use erase() with object", json::type_error&);
CHECK_THROWS_WITH_AS(d.erase(root["a"], 2), "[json.exception.out_of_range.401] array index 2 is out of range", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("")), "[json.exception.out_of_range.405] JSON pointer has no parent", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("/missing/x")), json_view_test::message("out_of_range.403", "", 0, 47, "key 'missing' not found").c_str(), json::out_of_range&);
CHECK_THROWS_WITH_AS(d.erase(json::json_pointer("/missing/x")), "[json.exception.out_of_range.403] key 'missing' not found", json::out_of_range&);
CHECK_THROWS_WITH_AS(d.set(json::json_pointer("/a/01"), 1), "[json.exception.parse_error.106] parse error: array index '01' must not begin with '0'", json::parse_error&);
CHECK_THROWS_WITH_AS(d.set(root, json_editable_view()), "[json.exception.type_error.302] type must be a value, but is discarded", json::type_error&);
CHECK_THROWS_WITH_AS(d.set(root, json::binary({1, 2})), "[json.exception.type_error.319] cannot store a binary value in a json_document", json::type_error&);
@@ -941,123 +941,3 @@ TEST_CASE("json_view edits: duplicate keys")
CHECK(d.root()["o"]["d"].get<int>() == 0);
}
}
#if !defined(JSON_NOEXCEPTION)
TEST_CASE("json_view edits: check_utf8 agrees with the byte-wise check")
{
namespace dv = nlohmann::detail::view;
// "ok" or the exception message
const auto run = [](bool fast, const std::string & s) -> std::string
{
try
{
if (fast)
{
dv::check_utf8(s.data(), s.size());
}
else
{
dv::check_utf8_bytewise(reinterpret_cast<const unsigned char*>(s.data()), s.size(), 0);
}
}
catch (const json::type_error& e)
{
return {e.what()};
}
return {"ok"};
};
std::size_t errors = 0;
const auto same = [&](const std::string & s)
{
const std::string a = run(true, s);
const std::string b = run(false, s);
if (a != b)
{
INFO("fast: " << a << ", byte-wise: " << b << ", length " << s.size());
CHECK(a == b);
}
errors += b != "ok" ? 1u : 0u;
};
const std::vector<std::string> pieces =
{
"a", "Z", " ", "\"", "\\", "\n", "\x01", "\x7f", "\xc3\xa4", "\xe4\xb8\xad", "\xf0\x9f\x98\x80",
"\x80", "\xbf", "\xc0\x80", "\xc1\xbf", "\xc2", "\xe0\x80\x80", "\xe0\x9f\xbf", "\xed\xa0\x80", "\xed\xbf\xbf",
"\xf0\x80\x80\x80", "\xf0\x8f\xbf\xbf", "\xf4\x90\x80\x80", "\xf4\x8f\xbf\xbf", "\xf5", "\xf8", "\xff", "\xfe",
"\xe4\xb8", "\xf0\x9f\x98", "\xf0\x9f", "\xf0"
};
// every piece behind prefixes of every length, and followed by more bytes
for (std::size_t prefix = 0; prefix <= 64; ++prefix)
{
for (const auto& piece : pieces)
{
same(std::string(prefix, 'x') + piece);
same(std::string(prefix, 'x') + piece + "yz");
same(std::string(prefix, '"') + piece);
same(std::string(prefix, '\xc3') + piece);
same(std::string(prefix / 2, '\xc3').append(prefix / 2, '\xa4') + piece);
same(std::string(prefix, 'x') + "\xe4\xb8\xad" + piece);
}
}
// an invalid byte at every position of valid strings of every length
for (std::size_t len = 0; len <= 64; ++len)
{
const std::string ascii(len, 'a');
std::string mixed;
while (mixed.size() < len)
{
if (mixed.size() % 7 == 0)
{
mixed += "\xc3\xa4";
}
else if (mixed.size() % 11 == 0)
{
mixed += "\xf0\x9f\x98\x80";
}
else
{
mixed += "a";
}
}
same(ascii);
same(mixed);
for (std::size_t pos = 0; pos < len; ++pos)
{
for (const char bad :
{'\x80', '\xc0', '\xc2', '\xe0', '\xf4', '\xff', '"', '\\', '\x1f'
})
{
std::string s = ascii;
s[pos] = bad;
same(s);
s = mixed;
s[pos] = bad;
same(s);
}
}
}
// random bytes, biased to the interesting ones
std::mt19937 gen(5869); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed): reproducible
static const std::array<unsigned char, 23> interesting = {{0x00, 0x1f, 0x22, 0x5c, 0x7f, 0x80, 0x8f, 0x90, 0x9f, 0xa0, 0xbf, 0xc0, 0xc2, 0xdf, 0xe0, 0xe1, 0xed, 0xef, 0xf0, 0xf1, 0xf4, 0xf5, 0xff}};
for (int i = 0; i < 100000; ++i)
{
std::string s(gen() % 80, 'a');
for (auto& c : s)
{
const unsigned r = gen() % 8;
if (r == 0)
{
c = static_cast<char>(gen() & 0xff);
}
else if (r == 1)
{
c = static_cast<char>(interesting[gen() % interesting.size()]);
}
}
same(s);
}
CHECK(errors > 1000);
}
#endif
+7 -4
View File
@@ -337,7 +337,8 @@ TEST_CASE("json_view from_json")
CHECK(d.root().get<std::unordered_map<std::string, int>>().at("a") == 3);
}
#if !defined(JSON_NOEXCEPTION)
// (the exceptions of views have neither the path nor the byte range of the value)
#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS
SECTION("std types: the same errors as from the materialized value")
{
const json_document d = json_document::parse(R"([[1, "x"], {"a": 1}, "text", [1], 5, null])");
@@ -402,10 +403,10 @@ TEST_CASE("json_view from_json")
CHECK(d.root()[4].get<strict_color>() == strict_color::red);
#if !defined(JSON_NOEXCEPTION)
const json j = d.root().materialize();
#if !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS
CHECK(exception_of_call([&] { static_cast<void>(d.root()[2].get<strict_color>()); }) == "[json.exception.out_of_range.410] enum value out of range for strict_color: \"purple\"");
#endif
#if !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS
CHECK(exception_of_call([&] { static_cast<void>(d.root()[2].get<strict_color>()); }) == exception_of_call([&] { static_cast<void>(j[2].get<strict_color>()); }));
#endif
#endif
}
@@ -451,9 +452,11 @@ TEST_CASE("json_view from_json")
CHECK(o.root().get<shape>().center.y == 2);
}
#if !defined(JSON_NOEXCEPTION)
#if !defined(JSON_NOEXCEPTION) && !JSON_DIAGNOSTICS && !JSON_DIAGNOSTIC_POSITIONS
SECTION("errors are those of the same conversion of a json value")
{
// (with JSON_DIAGNOSTICS, those of json name the path of the value,
// and with JSON_DIAGNOSTIC_POSITIONS its bytes)
const json_document d = json_document::parse(R"([{"x": 1}, {"x": "1", "y": 2}, [1, 2], {"id": "7", "c": "red"}])");
const json j = d.root().materialize();
for (std::size_t i = 0; i < 3; ++i)
-58
View File
@@ -701,64 +701,6 @@ TEST_CASE("json_view images: ownership")
#if !defined(JSON_NOEXCEPTION)
TEST_CASE("json_view images: errors")
{
SECTION("float tokens: a changed byte is accepted if the text is still a number with the digits the node records")
{
for (const char* const token :
{
"-12.375", "0.5", "123456.0", "-0.0", "9999999999999999999.5", "0.000000000000000000000001", "1.25e10", "5E-3"
})
{
CAPTURE(token)
const std::string text = std::string("[") + token + "]";
const json_document d = json_document::parse(text);
const std::vector<std::uint8_t> image = d.save();
const std::size_t at = text_at(image) + 1; // the first byte of the token
const std::uint16_t recorded = node_at(image, 1).extra;
CHECK(loaded_dump(image) == json::parse(text).dump());
for (std::size_t pos = 0; token[pos] != '\0'; ++pos)
{
for (unsigned int byte = 0; byte < 256; ++byte)
{
std::vector<std::uint8_t> changed = image;
changed[at + pos] = static_cast<std::uint8_t>(byte);
std::string t = token;
t[pos] = static_cast<char>(byte);
// the digits of the integer part and of the fraction of the changed token
std::size_t integer = 0;
std::size_t fraction = 0;
std::size_t k = (t[0] == '-') ? 1 : 0;
for (; k < t.size() && t[k] >= '0' && t[k] <= '9'; ++k)
{
++integer;
}
if (k < t.size() && t[k] == '.')
{
for (++k; k < t.size() && t[k] >= '0' && t[k] <= '9'; ++k)
{
++fraction;
}
}
// (as a JSON text, the token may have white space around it)
const bool float_token = json::accept(t) && t.find_first_of(std::string(" \t\r\n\0", 5)) == std::string::npos && (t.find('.') != std::string::npos || t.find_first_of("eE") != std::string::npos) // NOLINT(abseil-string-find-str-contains)
&& ((integer | (fraction << 8U)) == recorded);
CAPTURE(pos)
CAPTURE(byte)
std::string result;
try
{
result = loaded_dump(changed);
}
catch (const json::parse_error& e)
{
CHECK(std::string(e.what()) == check_failed);
result = "rejected";
}
CHECK((result != "rejected") == float_token);
}
}
}
}
SECTION("invalid UTF-8 of a decoded string: the compact dump writes it, the others throw")
{
const json_document d = json_document::parse(R"({"k\n":"\u00e9x"})");
-226
View File
@@ -81,112 +81,6 @@ std::string range(const json& j)
{
return j.start_pos() == npos && j.end_pos() == npos ? "none" : std::to_string(j.start_pos()) + "-" + std::to_string(j.end_pos());
}
#if !defined(JSON_NOEXCEPTION)
using json_view_test::exception_of_call;
// the exceptions about every value of v are those about the value at the same
// place in j, a value of the whole document from parse() or materialize() (so
// that its parents and positions are set); members whose key repeats are
// skipped (the view finds the first one, j holds the last)
template<typename View>
void check_exceptions(const View& v, const json& j)
{
CAPTURE(j.dump())
if (!v.is_number())
{
CHECK(exception_of_call([&] { static_cast<void>(v.template get<int>()); }) == exception_of_call([&] { static_cast<void>(j.get<int>()); }));
}
if (!v.is_string())
{
CHECK(exception_of_call([&] { static_cast<void>(v.template get<std::string>()); }) == exception_of_call([&] { static_cast<void>(j.get<std::string>()); }));
}
if (!v.is_array())
{
CHECK(exception_of_call([&] { static_cast<void>(v.template get<std::vector<int>>()); }) == exception_of_call([&] { static_cast<void>(j.get<std::vector<int>>()); }));
}
CHECK(exception_of_call([&] { static_cast<void>(v.at("missing")); }) == exception_of_call([&] { static_cast<void>(j.at("missing")); }));
CHECK(exception_of_call([&] { static_cast<void>(v.at(1000)); }) == exception_of_call([&] { static_cast<void>(j.at(1000)); }));
CHECK(exception_of_call([&] { static_cast<void>(v.at(json::json_pointer("/missing"))); }) == exception_of_call([&] { static_cast<void>(j.at(json::json_pointer("/missing"))); }));
CHECK(exception_of_call([&] { static_cast<void>(v.at(json::json_pointer("/-"))); }) == exception_of_call([&] { static_cast<void>(j.at(json::json_pointer("/-"))); }));
CHECK(exception_of_call([&] { static_cast<void>(v.value("k", 0)); }) == exception_of_call([&] { static_cast<void>(j.value("k", 0)); }));
if (!v.is_object())
{
CHECK(exception_of_call([&] { static_cast<void>(v["k"]); }) == exception_of_call([&] { static_cast<void>(j["k"]); }));
// (without the context of basic_json: view iterators do not know the value they iterate over)
CHECK(exception_of_call([&] { static_cast<void>(v.begin().key()); }) == "[json.exception.invalid_iterator.207] cannot use key() for non-object iterators");
}
if (!v.is_array())
{
CHECK(exception_of_call([&] { static_cast<void>(v[0]); }) == exception_of_call([&] { static_cast<void>(j[0]); }));
}
if (v.is_null())
{
CHECK(exception_of_call([&] { static_cast<void>(v.front()); }) == exception_of_call([&] { static_cast<void>(j.front()); }));
}
if (v.is_object())
{
for (auto it = v.begin(); it != v.end(); ++it)
{
const std::string key(it.key().data(), it.key().size());
if (v.count(key) == 1)
{
check_exceptions(it.value(), j.at(key));
}
}
}
else if (v.is_array())
{
for (std::size_t i = 0; i < v.size(); ++i)
{
check_exceptions(v[i], j[i]);
}
}
}
// as check_exceptions, for edits that fail (they change neither d nor j)
void check_edit_exceptions(json_editable_document& d, const json_editable_document::view_type& v, json& j)
{
CAPTURE(j.dump())
if (!v.is_object() && !v.is_null())
{
CHECK(exception_of_call([&] { d.set(v, "k", 1); }) == exception_of_call([&] { j["k"] = 1; }));
CHECK(exception_of_call([&] { d.erase(v, "k"); }) == exception_of_call([&] { j.erase("k"); }));
}
if (!v.is_array() && !v.is_null())
{
CHECK(exception_of_call([&] { d.set(v, 0, 1); }) == exception_of_call([&] { j[0] = 1; }));
CHECK(exception_of_call([&] { d.push_back(v, 1); }) == exception_of_call([&] { j.push_back(1); }));
}
if (!v.is_array())
{
CHECK(exception_of_call([&] { d.insert(v, 0, 1); }) == exception_of_call([&] { j.insert(j.cbegin(), 1); }));
CHECK(exception_of_call([&] { d.erase(v, 0); }) == exception_of_call([&] { j.erase(0); }));
}
else
{
CHECK(exception_of_call([&] { d.erase(v, 1000); }) == exception_of_call([&] { j.erase(1000); }));
}
if (v.is_object())
{
for (auto it = v.begin(); it != v.end(); ++it)
{
const std::string key(it.key().data(), it.key().size());
if (v.count(key) == 1)
{
check_edit_exceptions(d, it.value(), j.at(key));
}
}
}
else if (v.is_array())
{
for (std::size_t i = 0; i < v.size(); ++i)
{
check_edit_exceptions(d, v[i], j[i]);
}
}
}
#endif
} // namespace
TEST_CASE("json_view materialize() source positions")
@@ -451,123 +345,3 @@ TEST_CASE("json_view materialize() source positions")
}
#endif
}
#if !defined(JSON_NOEXCEPTION)
TEST_CASE("json_view exceptions name the path and the bytes of the value")
{
SECTION("as those of basic_json")
{
const std::vector<std::string> texts =
{
"null", "true", "42", "\"s\"", "[]", "{}", "[1, [2, [3, []]]]", R"({"a": {"b": [1, {"c": null}]}, "d": "e"})",
R"({"a/b": {"~": [1]}, "\u0041\n": {"x": "\t"}, "": [{}]})",
R"([1 /* ] */, {"a": [] // }
, "b": {} } , ])",
"\n\t[\r\n 1 ,\n {\n \"a\" : [ ]\n }\n]\n",
};
for (const auto& text : texts)
{
CAPTURE(text)
const json j = json::parse(text, nullptr, true, true, true);
const json_document d = json_document::parse(text, true, true, true);
check_exceptions(d.root(), j);
json_editable_document e = json_editable_document::parse(text, true, true, true);
check_exceptions(e.root(), j);
json m = e.root().materialize();
check_edit_exceptions(e, e.root(), m);
}
}
SECTION("generated documents")
{
json_view_test::generator gen;
for (int i = 0; i < 100; ++i)
{
std::string text;
gen.value(text, 0);
CAPTURE(text)
const json_document d = json_document::parse(text);
check_exceptions(d.root(), json::parse(text));
}
}
SECTION("edited documents: as those of materialize()")
{
const std::string text = R"({"a": [1, 2], "b": {"c": "\n", "d": true}, "e": [3, [4]], "f": "x"})";
json_editable_document d = json_editable_document::parse(text);
d.set(d.root()["f"], 5);
d.push_back(d.root()["a"], json::parse(R"({"n": [null]})"));
d.erase(d.root()["e"], 0);
d.set(d.root()["b"], "c2", d.root()["e"]);
d.insert(d.root()["e"], 0, "new");
json m = d.root().materialize();
check_exceptions(d.root(), m);
check_edit_exceptions(d, d.root(), m);
// (a value inside a new container: a path, but no bytes)
std::string expected = "[json.exception.type_error.302] ";
#if JSON_DIAGNOSTICS
expected += "(/a/2/n/0) ";
#endif
expected += "type must be number, but is null";
CHECK(exception_of_call([&] { static_cast<void>(d.root()["a"][2]["n"][0].get<int>()); }) == expected);
}
SECTION("deep documents with siblings after every nested container")
{
// the search for the path pushes and pops one frame per level (the
// stack reallocates while it grows, and shrinks again at every
// sibling): run under a sanitizer, a reference to a popped or moved
// frame would show here
std::string text = "7";
for (int level = 0; level < 40; ++level)
{
text = R"([[1, [2]], )" + text + R"(, {"k": [3, {"l": [4]}]}, [5, [6, [7]]]])";
}
CAPTURE(text)
const json j = json::parse(text);
const json_document d = json_document::parse(text);
check_exceptions(d.root(), j);
json_editable_document e = json_editable_document::parse(text);
e.set(e.root()[0][0], 9);
check_exceptions(e.root(), e.root().materialize());
}
SECTION("examples")
{
const std::string text = R"({"people": [{"name": "a", "age": 1}, {"name": "c", "age": "x"}], "o": {"k": 1}})";
const json_document d = json_document::parse(text);
std::string expected_age = "[json.exception.type_error.302] ";
#if JSON_DIAGNOSTICS
expected_age += "(/people/1/age) ";
#endif
expected_age += "(bytes 58-61) type must be number, but is string";
CHECK(exception_of_call([&] { static_cast<void>(d.root()["people"][1]["age"].get<int>()); }) == expected_age);
std::string expected_zz = "[json.exception.out_of_range.403] ";
#if JSON_DIAGNOSTICS
expected_zz += "(/people/0) ";
#endif
expected_zz += "(bytes 12-35) key 'zz' not found";
CHECK(exception_of_call([&] { static_cast<void>(d.root().at(json::json_pointer("/people/0/zz"))); }) == expected_zz);
// the root has no path, and with JSON_DIAGNOSTICS then no bytes either (as basic_json)
std::string expected_missing = "[json.exception.out_of_range.403] ";
#if !JSON_DIAGNOSTICS
expected_missing += "(bytes 0-79) ";
#endif
expected_missing += "key 'missing' not found";
CHECK(exception_of_call([&] { static_cast<void>(d.root().at("missing")); }) == expected_missing);
}
SECTION("duplicate keys: the value the view finds")
{
const json_document d = json_document::parse(R"({"x": "1", "x": "22"})");
std::string expected = "[json.exception.type_error.302] ";
#if JSON_DIAGNOSTICS
expected += "(/x) ";
#endif
expected += "(bytes 6-9) type must be number, but is string";
CHECK(exception_of_call([&] { static_cast<void>(d.root()["x"].get<int>()); }) == expected);
}
}
#endif
+46
View File
@@ -23,6 +23,7 @@ using nlohmann::json;
#include <cstdint>
#include <sstream>
#include <iomanip>
#include <limits>
#include <string>
#include <vector>
@@ -248,6 +249,51 @@ TEST_CASE_TEMPLATE("serialization for extreme integer values", T, int32_t, uint3
}
}
TEST_CASE("integers of every digit count are written like std::to_string")
{
// dump_integer() writes up to 16 digits in two groups of eight and 17 to
// 20 digits as one to four digits and two groups of eight
std::vector<std::uint64_t> values;
std::uint64_t power = 1;
for (int digits = 1; digits <= 20; ++digits)
{
values.push_back(power); // 1 followed by zeros
values.push_back(power + 1);
values.push_back((power * 2) - 1);
if (digits < 20)
{
values.push_back((power * 10) - 1); // all 9s
values.push_back((power * 10) - power); // 9 followed by zeros
power *= 10;
}
}
values.push_back((std::numeric_limits<std::uint64_t>::max)());
values.push_back(static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()));
values.push_back(static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()) + 1);
std::uint64_t state = 0x9E3779B97F4A7C15u;
for (int i = 0; i < 20000; ++i)
{
state = (state * 6364136223846793005u) + 1442695040888963407u;
values.push_back(state >> (state % 61)); // every bit length
}
for (const auto v : values)
{
CAPTURE(v)
CHECK(json(v).dump() == std::to_string(v));
if (v <= static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()))
{
const auto s = static_cast<std::int64_t>(v);
CHECK(json(s).dump() == std::to_string(s));
CHECK(json(-s).dump() == std::to_string(-s));
}
}
CHECK(json((std::numeric_limits<std::int64_t>::min)()).dump() == "-9223372036854775808");
// in a container, so the digits are followed by other output
CHECK(json::array({std::uint64_t{18446744073709551615u}, std::int64_t{-1234567890123456789}, 1}).dump() == "[18446744073709551615,-1234567890123456789,1]");
}
TEST_CASE("dump with binary values")
{
auto binary = json::binary({1, 2, 3, 4});