news 2026/9/8 20:25:53

JSON for Modern C++ 中 basic_json::start_pos() 完全指南:定位解析源字符串中每个 JSON 值的起始位置

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSON for Modern C++ 中 basic_json::start_pos() 完全指南:定位解析源字符串中每个 JSON 值的起始位置

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_positionend_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时为tfalse时为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_backerase、替换等变更,内部记录的start_position/end_position不会自动更新,继续使用旧位置去索引源字符串将产生错误结果。

这也是源码中start_positionend_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

逐条解读输出,可以直观理解位置的语义:

  1. 根节点start_pos: 5:原始字符串以换行与 4 个空格开头(位置 0–4),根对象的{恰好位于下标 5;end_pos: 109是闭括号}之后的位置。
  2. 嵌套对象"address"start_pos: 26对应该对象{的偏移;对j["address"]substr,切出来的正是它自己的{...}子文本。
  3. 字符串"street"start_pos: 50end_pos: 63,区间[50, 63)恰好覆盖带引号的"Fake Street"(注意此处包含首尾引号)。
  4. 数值housenumberstart_pos: 92end_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第一个字符最后一个字符之后
booleant/fe之后
nullnl之后

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. 使用建议与注意事项小结

  1. 启用时机:仅在需要“解析树 ↔ 源文本映射”或增强错误定位时开启;因每个 JSON 值会增加两个std::size_t成员及轻微运行时开销,追求极致的存储/性能场景应保持默认关闭。
  2. 生效范围:位置只对parse()创建的值为有效,sax_parse()、手工构造的值返回std::string::npos,调用前可用!= std::string::npos防御。
  3. 只读快照:修改 JSON 值后旧位置不再有效,应避免跨变更复用位置做切片/定位。
  4. 字节语义:位置按字节计(std::size_t),拼接进异常消息时呈现为(bytes start-end);配合substr使用最简单可靠。
  5. 配套接口:总是与end_pos()成对使用来推导闭区间;区间切片[start_pos(), end_pos())是还原原文的标准写法。

版本历史

  • start_pos()与配套的JSON_DIAGNOSTIC_POSITIONSend_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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 20:24:22

9款Claude Code插件实测:从上下文压缩到自动化编排,好用才留

这两年 Claude Code 的火爆程度&#xff0c;相信不用我多说了。命令行里跑 AI 编程助手&#xff0c;已经从“极客玩具”变成了不少人日常工作的标配。但项目火了&#xff0c;插件生态自然也跟着热闹起来&#xff0c;GitHub 上随便一搜就是一大堆号称“提效十倍”的插件&#xf…

作者头像 李华