JSON for Modern C++ 中 basic_json::start_pos() 完全指南:定位解析源字符串中每个 JSON 值的起始位置
【免费下载链接】jsonJSON for Modern C++项目地址: https://gitcode.com/GitHub_Trending/js/json
start_pos()是 nlohmann/basic_json 提供的一项诊断定位能力:当编译期开启宏JSON_DIAGNOSTIC_POSITIONS后,由parse解析出的每一个 JSON 值都会记录“它在原始输入字符串中的起始字节位置”,从而让你能够把解析后的basic_json树重新映射回源文本,进行精确的源码定位、切片回显或错误报告。读完本文,你将掌握start_pos()的启用前提、各 JSON 类型下的返回值语义、底层实现机制、与end_pos()的配合用法,以及它在异常诊断中的实际作用。
1. 函数声明与启用前提
start_pos()并非默认就存在。它的声明包裹在条件编译指令中,只有宏JSON_DIAGNOSTIC_POSITIONS被定义为1时该成员函数才会出现在basic_json中:
#if JSON_DIAGNOSTIC_POSITIONS constexpr std::size_t start_pos() const noexcept; #endif对应的官方文档位于 docs/mkdocs/docs/api/basic_json/start_pos.md。该函数自版本 3.12.0起加入,具有以下特性:
constexpr:可用在编译期常量表达式中;noexcept:见下文“异常安全”;- 返回类型
std::size_t,本质是一个非负字节下标(std::string::size_type)。
1.1 必须开启 JSON_DIAGNOSTIC_POSITIONS 宏
在#include <nlohmann/json.hpp>之前定义宏即可启用,例如:
#define JSON_DIAGNOSTIC_POSITIONS 1 #include <nlohmann/json.hpp>- 该宏默认值为
0(诊断定位默认关闭),详见 JSON_DIAGNOSTIC_POSITIONS 宏文档。 - 也可通过 CMake 选项
JSON_Diagnostic_Positions(默认OFF)控制,开启后它会为编译目标自动定义该宏。 - 开启后,每个
basic_json值内部会额外增加两个std::size_t字段来保存起始/结束位置,同时解析、拷贝以及对异常消息的生成会有轻微额外开销。这一点可以在源码成员声明中得到印证:include/nlohmann/json.hpp#L4313-L4328 中,字段start_position与end_position被初始化为std::string::npos,并由公有成员函数返回。
1.2 注意:sax_parse 不记录位置
根据 JSON_DIAGNOSTIC_POSITIONS 宏文档 的说明,只有通过parse创建的 JSON 值会带有位置信息;sax_parse以及其他一切手工构造方式(初始化列表、赋值、拷贝等)都不会设置诊断位置,此时start_pos()恒返回std::string::npos。
2. 返回值语义:start_pos() 返回什么
start_pos()返回的是该值在“被解析的原始 JSON 字符串”中第一个字符的字节位置。所谓“值”既可以是根节点,也可以是经由operator[]、迭代器等访问到的任意嵌套对象、数组或标量。
针对不同 JSON 类型,起始位置指向的具体字符如下(原文表格完整继承自 start_pos.md):
| JSON 类型 | 返回值含义 |
|---|---|
| object(对象) | 开括号{的位置 |
| array(数组) | 开括号[的位置 |
| string(字符串) | 开引号"的位置 |
| number(数值) | 第一个字符的位置 |
| boolean(布尔) | true时为t、false时为f的位置 |
| null | 字母n的位置 |
位置始终是相对最初喂给
parse()的那段源文本(含前导空白)的字节偏移,而非去除空白后的逻辑位置。若该值不是由parse创建,则返回std::string::npos。
2.1 返回类型为何用 npos 表示“无位置”
源码 include/nlohmann/json.hpp#L4315 中成员字段默认值即std::string::npos,因此对任何未参与parse过程的值,start_pos()与end_pos()都天然返回该哨兵值。这让你可以在代码中统一判断:
if (j.start_pos() != std::string::npos) { // 该值确实来自 parse,位置信息有效 }3. 异常安全与复杂度
- 异常安全:
start_pos()满足no-throw guarantee,任何情况下都不会抛出异常——它只是返回一个内部std::size_t成员; - 复杂度:常量时间(O(1)),因为位置在解析阶段就已确定并缓存,查询阶段只是简单读取。
4. 底层实现:位置在解析阶段如何被记录
虽然用户侧只是读取一个成员,但位置数据的写入发生在解析(SAX)流程内,见 include/nlohmann/detail/input/json_sax.hpp。这里以对象为例说明实现逻辑:
- 在
start_object()中,当 lexer 已读到对象首字符后,用m_lexer_ref->get_position() - 1回退一位作为起始位置(对应开括号{); - 在
end_object()中,lexer 已经越过闭括号},因此直接把当前读取位置作为结束位置; - 数组、字符串、数值、布尔、null 等各有对应分支(例如布尔值通过
end_position - 4或- 5反推出t/f所在起始位置)。
这说明start_pos()/end_pos()本质是解析期副产品:它们不是解析完成后重新扫描得到的,而是 SAX 事件触发时顺带打下的“时间戳”。因此只有当值经由完整parse()路径创建时位置才存在,与上文 1.2 的说明完全吻合。
5. 位置的有效性:修改即失效
!!! warning 重要约束 返回的位置仅在 JSON 值未被修改时有效。一旦对值进行赋值、push_back、erase、替换等变更,内部记录的start_position/end_position不会自动更新,继续使用旧位置去索引源字符串将产生错误结果。
这也是源码中start_position、end_position只是普通非mutable成员、且没有任何“变更后重算”逻辑的原因。请把位置信息当作解析时刻的只读快照来使用。
6. 完整示例:定位并切片回显源文本
官方示例源码位于 docs/mkdocs/docs/examples/diagnostic_positions.cpp,其完整可运行代码如下(end_pos()返回的是“末字符之后的那个位置”,因此用end_pos() - start_pos()即可得到精确长度,配合std::string::substr把原始子文本原样切出来):
#include <iostream> #define JSON_DIAGNOSTIC_POSITIONS 1 #include <nlohmann/json.hpp> using json = nlohmann::json; int main() { std::string json_string = R"( { "address": { "street": "Fake Street", "housenumber": 1 } } )"; json j = json::parse(json_string); std::cout << "Root diagnostic positions: \n"; std::cout << "\tstart_pos: " << j.start_pos() << '\n'; std::cout << "\tend_pos:" << j.end_pos() << "\n"; std::cout << "Original string: \n"; std::cout << "{\n \"address\": {\n \"street\": \"Fake Street\",\n \"housenumber\": 1\n }\n }" << "\n"; std::cout << "Parsed string: \n"; std::cout << json_string.substr(j.start_pos(), j.end_pos() - j.start_pos()) << "\n\n"; std::cout << "address diagnostic positions: \n"; std::cout << "\tstart_pos:" << j["address"].start_pos() << '\n'; std::cout << "\tend_pos:" << j["address"].end_pos() << "\n\n"; std::cout << "Original string: \n"; std::cout << "{ \"street\": \"Fake Street\",\n \"housenumber\": 1\n }" << "\n"; std::cout << "Parsed string: \n"; std::cout << json_string.substr(j["address"].start_pos(), j["address"].end_pos() - j["address"].start_pos()) << "\n\n"; std::cout << "street diagnostic positions: \n"; std::cout << "\tstart_pos:" << j["address"]["street"].start_pos() << '\n'; std::cout << "\tend_pos:" << j["address"]["street"].end_pos() << "\n\n"; std::cout << "Original string: \n"; std::cout << "\"Fake Street\"" << "\n"; std::cout << "Parsed string: \n"; std::cout << json_string.substr(j["address"]["street"].start_pos(), j["address"]["street"].end_pos() - j["address"]["street"].start_pos()) << "\n\n"; std::cout << "housenumber diagnostic positions: \n"; std::cout << "\tstart_pos:" << j["address"]["housenumber"].start_pos() << '\n'; std::cout << "\tend_pos:" << j["address"]["housenumber"].end_pos() << "\n\n"; std::cout << "Original string: \n"; std::cout << "1" << "\n"; std::cout << "Parsed string: \n"; std::cout << json_string.substr(j["address"]["housenumber"].start_pos(), j["address"]["housenumber"].end_pos() - j["address"]["housenumber"].start_pos()) << "\n\n"; }对应的标准输出见 docs/mkdocs/docs/examples/diagnostic_positions.output:
Root diagnostic positions: start_pos: 5 end_pos:109 ... Parsed string: { "address": { "street": "Fake Street", "housenumber": 1 } } address diagnostic positions: start_pos:26 end_pos:103 ... Parsed string: { "street": "Fake Street", "housenumber": 1 } street diagnostic positions: start_pos:50 end_pos:63 ... Parsed string: "Fake Street" housenumber diagnostic positions: start_pos:92 end_pos:93 ... Parsed string: 1逐条解读输出,可以直观理解位置的语义:
- 根节点
start_pos: 5:原始字符串以换行与 4 个空格开头(位置 0–4),根对象的{恰好位于下标 5;end_pos: 109是闭括号}之后的位置。 - 嵌套对象
"address":start_pos: 26对应该对象{的偏移;对j["address"]取substr,切出来的正是它自己的{...}子文本。 - 字符串
"street":start_pos: 50、end_pos: 63,区间[50, 63)恰好覆盖带引号的"Fake Street"(注意此处包含首尾引号)。 - 数值
housenumber:start_pos: 92、end_pos: 93,长度 1,正是字符1。
由于示例中多次用
substr(start_pos, end_pos - start_pos)验证,输出里 "Parsed string" 与源文本片段完全一致,反过来也证明了位置记录的高精度:即使嵌套、含大量空白,区间仍能精确命中每个值。
7. 与 end_pos() 配合:确定值在源中的完整区间
start_pos()与end_pos()是一对互补接口:
end_pos()返回“紧跟该值最后一个字符之后”的位置;- 因此区间
[start_pos(), end_pos())就是该值在原始 JSON 文本中的完整半开区间,长度等于end_pos() - start_pos()(含对象/数组的括号或字符串的引号)。
对任意 JSON 类型都有确定的区间端点,见下表(合并自 end_pos.md 与 JSON_DIAGNOSTIC_POSITIONS 两处文档):
| JSON 类型 | start_pos() 指向 | end_pos() 指向 |
|---|---|---|
| object | 开括号{ | 闭括号}之后 |
| array | 开括号[ | 闭括号]之后 |
| string | 开引号" | 闭引号"之后 |
| number | 第一个字符 | 最后一个字符之后 |
| boolean | t/f | e之后 |
| null | n | l之后 |
7.1 典型用法场景
- 把解析树反投影回原文件:定位某字段对应的原文行/列,用于自定义的 lint 或 AST 式工具;
- 精确切片:
text.substr(j[key].start_pos(), j[key].end_pos() - j[key].start_pos())可无损还原该字段的原始书写形式(含引号、原始数值格式),不会因重新 dump 造成格式差异; - 调试与错误报告:将问题值的位置直接告诉用户。
8. 诊断位置在异常消息中的应用
开启JSON_DIAGNOSTIC_POSITIONS后,异常消息会自动带上触发异常的那个叶子值的字节区间。相关实现位于 include/nlohmann/detail/exceptions.hpp#L144-L161:get_byte_positions()会检查叶子元素的start_pos()与end_pos()是否都不等于npos,若是则生成形如(bytes 起点-终点)的前缀拼进错误描述。
官方示例 diagnostic_positions_exception.cpp 演示了把"housenumber": "1"读成int时抛出的类型错误,输出见 diagnostic_positions_exception.output:
[json.exception.type_error.302] (bytes 92-95) type must be number, but is string即异常文本中直接给出了出问题字段在原始 JSON 中的字节区间(bytes 92-95)。若再叠加宏JSON_DIAGNOSTICS(参见 json_diagnostics 宏文档),异常还能同时携带从根到叶子节点的路径信息与字节区间,示例见 diagnostics_extended_positions.cpp 及其 对应输出。对于解析器、配置校验、编译器类工具,这能让报错信息直接定位到“原文件的哪个字节范围出了问题”。
9. 测试验证:位置的精确性有据可查
仓库中的测试覆盖了对位置语义的严格校验:
- tests/src/unit-diagnostic-positions.cpp 第 12 行开启
JSON_DIAGNOSTIC_POSITIONS 1,第 51 行用text.substr(v.start_pos(), v.end_pos() - v.start_pos()) == token断言切片结果与原文 token 完全一致,第 67 行断言根值j.start_pos() == 0; - tests/src/unit-class_parser_diagnostic_positions.cpp 第 319 行等大量用例验证了嵌套对象、数组元素的位置区间(如第 349、1802–1817 行),并以“子文本切片等于原字符串子串”作为判定标准;
- 第 1953 行
CHECK(j.start_pos() == initial_whitespace.size())确认位置计算正确处理了前导空白。
这些测试共同佐证:start_pos()返回的是含前导空白在内的原始字节偏移,且对任意嵌套层级与复合类型都成立。
10. 使用建议与注意事项小结
- 启用时机:仅在需要“解析树 ↔ 源文本映射”或增强错误定位时开启;因每个 JSON 值会增加两个
std::size_t成员及轻微运行时开销,追求极致的存储/性能场景应保持默认关闭。 - 生效范围:位置只对
parse()创建的值为有效,sax_parse()、手工构造的值返回std::string::npos,调用前可用!= std::string::npos防御。 - 只读快照:修改 JSON 值后旧位置不再有效,应避免跨变更复用位置做切片/定位。
- 字节语义:位置按字节计(
std::size_t),拼接进异常消息时呈现为(bytes start-end);配合substr使用最简单可靠。 - 配套接口:总是与
end_pos()成对使用来推导闭区间;区间切片[start_pos(), end_pos())是还原原文的标准写法。
版本历史
start_pos()与配套的JSON_DIAGNOSTIC_POSITIONS、end_pos()一同在版本 3.12.0中加入(见 start_pos.md 与 end_pos.md 的 Version history)。- 使用前请确认你的 nlohmann/json 头文件版本不低于 3.12.0,且编译时通过宏或 CMake 选项
JSON_Diagnostic_Positions开启该特性。
【免费下载链接】jsonJSON for Modern C++项目地址: https://gitcode.com/GitHub_Trending/js/json
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考