news 2026/9/7 9:45:14

JSON for Modern C++ 诊断位置详解:nlohmann::basic_json::end_pos 与 JSON_DIAGNOSTIC_POSITIONS 实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSON for Modern C++ 诊断位置详解:nlohmann::basic_json::end_pos 与 JSON_DIAGNOSTIC_POSITIONS 实战

JSON for Modern C++ 诊断位置详解:nlohmann::basic_json::end_pos 与 JSON_DIAGNOSTIC_POSITIONS 实战

【免费下载链接】jsonJSON for Modern C++项目地址: https://gitcode.com/GitHub_Trending/js/json

end_pos()是 nlohmann::basic_json 在开启JSON_DIAGNOSTIC_POSITIONS宏后可用的一个成员函数,用于返回某个 JSON 值在其来源 JSON 字符串中"最后一个字符之后的位置"。配合start_pos()使用,可以精确定位任意节点在原始输入中的字节区间,从而把解析结果与源文本片段一一映射。本文将完整讲清end_pos()的语义、不同 JSON 类型下的返回值规则、失效条件与代价,并深入源码剖析位置信息是如何在 SAX 解析器中写入的,帮助你在构建 JSON 编辑器高亮、错误回溯、增量解析等工具时可靠地落地这一特性。

函数签名与基本语义

官方 API 文档(见 end_pos)给出的声明是:

#if JSON_DIAGNOSTIC_POSITIONS constexpr std::size_t end_pos() const noexcept; #endif

即该函数只有在编译前把宏JSON_DIAGNOSTIC_POSITIONS定义为1时才会存在。它返回该 JSON 值被解析时,在原始 JSON 字符串中最后一个字符之后的那个位置(半开区间语义:位置本身不属于该值)。

不同 JSON 类型对应的返回值如下(表格完整继承自官方文档):

JSON 类型返回值
object位置在闭合}之后
array位置在闭合]之后
string位置在闭合"之后
number位置在最后一个字符之后
boolean位置在e之后
null位置在l之后

注意 boolean 与 null 的表述:true/false都以e结尾、nulll结尾,所以end_pos()恰好指向这四个字母之后的字节位置。

返回值规则:如果值是由parse函数创建的,则返回上述位置;如果值是通过其他方式构造的(字面量、push_back、其他构造函数等),则返回std::string::npos

异常安全:无抛出保证(No-throw guarantee),此成员函数从不抛出异常。时间复杂度:常量。

与之互补的start_pos()(见 start_pos)返回值的第一个字符位置。两者相减即为该值在源串中的完整长度(含开闭括号、引号):

std::size_t len = j.end_pos() - j.start_pos();

启用方式:宏定义与 CMake 选项

方式一:在包含头文件前定义宏

最简单的启用方式是在包含库头文件之前把宏定义为 1(注意必须在#include之前):

#define JSON_DIAGNOSTIC_POSITIONS 1 #include <nlohmann/json.hpp>

如果宏从未被用户定义,库会自行将其定义为一个默认值。从源码 abi_macros.hpp 可以看到默认值是0(关闭):

#ifndef JSON_DIAGNOSTIC_POSITIONS #define JSON_DIAGNOSTIC_POSITIONS 0 #endif

也就是说,位置诊断默认是关闭的,只有显式开启后才存在start_pos()/end_pos()这两个成员函数——不开启时直接调用它们会编译失败。

方式二:CMake 选项 JSON_Diagnostic_Positions

当以 CMake 子目录、FetchContentfind_package方式集成时,可以不用手动定义宏,而是使用 CMake 选项。项目根 CMakeLists.txt 中声明了该选项(默认OFF):

option(JSON_Diagnostic_Positions "Enable diagnostic positions." OFF)

开启后,该选项通过target_compile_definitionsnlohmann_json目标追加JSON_DIAGNOSTIC_POSITIONS=1的接口级定义(见 CMakeLists.txt 中的$<$<BOOL:${JSON_Diagnostic_Positions}>:JSON_DIAGNOSTIC_POSITIONS=1>)。配置文档 cmake.md 对这一节也有对应说明:

JSON_Diagnostic_Positions— Enable position diagnostics by defining macroJSON_DIAGNOSTIC_POSITIONS. This option isOFFby default.

典型用法:

cmake -S your_project -B build -DJSON_Diagnostic_Positions=ON # 子项目场景下对应 nlohmann_json 的选项

代价说明

启用该宏会带来额外开销,官方文档(见 JSON_DIAGNOSTIC_POSITIONS)明确指出:每个 JSON 值会多出两个std::size_t成员(start_positionend_position,源码中默认初始化为std::string::npos),解析、JSON 值对象的拷贝以及异常错误信息生成都会有轻微运行时开销;作为回报,这些位置信息也会出现在相关异常的错误消息中。因此建议只在确实需要源码位置信息的工具型项目(解析器、编辑器插件、LSP 等)中开启。

完整示例:提取任意节点对应的原始片段

下面这个完整示例继承自仓库中的 diagnostic_positions.cpp,演示了对根对象、嵌套对象、字符串字段、数字字段分别调用start_pos()/end_pos(),并用substr(start_pos(), end_pos() - start_pos())从原始字符串中精确切出该节点对应的子串:

#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"; }

对应输出(来自 diagnostic_positions.output):

Root diagnostic positions: start_pos: 5 end_pos:109 Original string: { "address": { "street": "Fake Street", "housenumber": 1 } } Parsed string: { "address": { "street": "Fake Street", "housenumber": 1 } } address diagnostic positions: start_pos:26 end_pos:103 Original string: { "street": "Fake Street", "housenumber": 1 } Parsed string: { "street": "Fake Street", "housenumber": 1 } street diagnostic positions: start_pos:50 end_pos:63 Original string: "Fake Street" Parsed string: "Fake Street" housenumber diagnostic positions: start_pos:92 end_pos:93 Original string: 1 Parsed string: 1

从输出可以验证几个关键点:

  • 根对象从第 5 字节(前导换行与空格之后)开始,到第 109 字节结束——即end_pos()指向闭合}之后的位置,与"半开区间"定义一致;
  • 字符串"Fake Street"的区间[50, 63)长度 13,正好覆盖开闭引号本身;
  • 数字1的区间[92, 93)长度 1;
  • 由于end_pos() - start_pos()得到的是完整文本长度,substr切出来的内容与源串中该字段的原文(含空白格式)完全一致。这意味着你保留了源文件中的原始排版,而不是dump()重新序列化后的结果——这对"只高亮/只替换被修改字段"这类需求非常有用。

源码剖析:位置信息是怎么写进去的

理解end_pos()为什么可靠,关键要看解析路径。在 json.hpp 中,开启宏后basic_json新增了私有成员和两个公开访问器:

#if JSON_DIAGNOSTIC_POSITIONS /// the start position of the value std::size_t start_position = std::string::npos; /// the end position of the value std::size_t end_position = std::string::npos; public: constexpr std::size_t start_pos() const noexcept { return start_position; } constexpr std::size_t end_pos() const noexcept { return end_position; } #endif

可以看到两个成员默认值都是std::string::npos,这正是"非 parse 创建则返回 npos"这一语义的直接实现。

真正的赋值发生在 DOM 解析器 json_sax.hpp 的json_sax_dom_parser中。该解析器在构造时可以接收一个lexer_t*m_lexer_ref),借助词法分析器的实时游标get_position()记录各回调时刻的字节位置:

  • 对象开始handle_object):词法分析器刚读完开括号,因此start_position = get_position() - 1指向{
  • 对象结束handle_end_object):词法分析器已越过闭括号,所以end_position = get_position(),即}之后的位置——这正是end_pos()文档中"position after the closing}"的实现来源;
  • 数组同理:handle_arrayget_position() - 1记录[handle_end_array记录]之后的位置;
  • 原始值(布尔、null、字符串、数字):handle_diagnostic_positions_for_json_value先用当前游标写入end_position,再按值的类型反推start_position。从源码结构看,其策略是按已知字面量长度倒推:boolean减去 4(true)或 5(false),null减去 4,string/数字等则减去词法单元文本长度get_string().size()。这个"先固定右端、再倒推左端"的方式保证了end_pos()始终与词法游标的绝对位置对齐,不依赖值的内部表示。

basic_json侧还需要保证位置随对象流转。源码 json.hpp 中,拷贝构造、移动构造和移动赋值都对start_position/end_position做了相应处理:拷贝构造直接拷贝两个成员;移动构造把位置搬给新对象后,将源对象的位置置回std::string::npos(即"移动后的原对象不再有位置");移动赋值则交换两个成员。这解释了为什么"移动后的旧引用再调用end_pos()会拿到 npos"。

此外,位置信息还服务于异常诊断。exceptions.hpp 中的get_byte_positions会在start_pos()end_pos()均非 npos 时拼出(bytes X-Y)前缀。仓库示例 diagnostic_positions_exception.output 展示的效果是:

[json.exception.type_error.302] (bytes 92-95) type must be number, but is string

也就是说,end_pos()不只是查询接口,它还是异常消息里字节区间的右端点。

关键限制:只对 parse 有效,且不随修改更新

使用end_pos()前必须牢记文档 JSON_DIAGNOSTIC_POSITIONS 中强调的两条约束:

  1. 只有parse会填充位置sax_parse以及一切其他方式(构造函数、字面量、push_back等)创建的 JSON 值不会设置诊断位置,其start_pos()/end_pos()一律返回std::string::npos。这与源码一致:位置写入逻辑全部位于依赖 lexer 游标的 DOM 解析回调中,而sax_parse路径不经过这些回调(除非你自行实现携带位置逻辑的 SAX 类)。因此判断"这个值是否可定位"的惯用法是:

    if (j.end_pos() != std::string::npos) { /* 可安全使用区间 */ }
  2. 位置失效(Invalidation)警告:返回的位置仅在 JSON 值不被修改时有效;值被修改后位置不会随之更新。从源码看,库并不在每次operator[]/push_back等变更操作后重新计算位置(那既昂贵也做不到,因为增量修改没有统一的"源串偏移"概念)。所以正确姿势是:解析后只读地查询位置,一旦要写回源文本,应基于"解析时的快照字符串 + 位置区间"做拼接或替换,而不是期待库自动维护一致性。

  3. 位置是相对于被解析的那份输入字符串,而非 JSON 文档的逻辑行/列。若输入本身是文件的一个片段(例如已经substr过),得到的偏移需要加上片段在文件中的起始位置才是文件级偏移。

测试用例中的行为验证

仓库的测试进一步印证了上述语义:

  • unit-diagnostic-positions.cpp 中对每个词法单元校验text.substr(v.start_pos(), v.end_pos() - v.start_pos()) == token,并对根对象断言j.end_pos() == root.size()——即根值的end_pos()恰好等于整个源串长度(无尾部空白时),与示例中"半开区间末端即字符串末尾"的行为吻合;
  • unit-class_parser_diagnostic_positions.cpp 则大量使用root.substr(start_pos(), end_pos() - start_pos())与预期子串比对,验证嵌套对象、嵌套数组各级区间都能精确还原源文本片段。

这两份测试可以作为你在自己项目中编写"位置回归测试"的模板。

小结

end_pos()本身只是一个constexprnoexcept、常数时间的简单访问器,但其背后是一套以词法游标为基准、在 SAX 解析回调中逐节点写入的位置追踪机制(见 json_sax.hpp)。实际使用时的要点:

  • 开启前提:编译前定义JSON_DIAGNOSTIC_POSITIONS 1,或使用 CMake 选项JSON_Diagnostic_Positions=ON,默认均为关闭;
  • 语义:半开区间右端点,end_pos() - start_pos()即该值在源串中的完整长度;
  • 可用边界:仅parse创建的值可定位,sax_parse与手工构造的值返回std::string::npos
  • 失效边界:值一旦被修改,位置不再更新,需要基于解析快照自行维护映射;
  • 额外收益:异常消息中附带(bytes X-Y)字节区间,便于定位解析/类型错误。

对于需要"从 JSON 值反向定位源文本"的工具链(编辑器集成、diff 生成、错误诊断),这两个函数是官方提供的标准入口,配套文档见 end_pos、start_pos 与 JSON_DIAGNOSTIC_POSITIONS。

【免费下载链接】jsonJSON for Modern C++项目地址: https://gitcode.com/GitHub_Trending/js/json

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

STM32F103C8T6点灯Demo硬核拆解:从GPIO到工程调试全流程

简介&#xff1a;一份基于STM32F103的32x64双色点阵屏静态显示演示工程&#xff0c;面向嵌入式显示驱动开发与STM32入门学习者。工程采用HUB08接口连接双色LED点阵屏&#xff0c;通过连续更新像素状态实现静态图像输出&#xff0c;覆盖系统时钟与GPIO初始化、PWM亮度控制、显示…

作者头像 李华
网站建设 2026/9/7 9:41:40

minimaxh3漫剧落地:ComfyUI工作流搭建与批量生产指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 9:40:06

赛车开奖动画实战:基于原生JS与CSS的状态机驱动实现

简介&#xff1a;赛车开奖动画源码是一套基于HTML5、CSS和JavaScript及jQuery实现的互动式赛车开奖展示程序&#xff0c;适合前端开发者、游戏爱好者或需要搭建趣味抽奖场景的运营人员学习与二次开发。资源以北京赛车为视觉主题&#xff0c;通过精致的PNG/GIF素材与CSS动画模拟…

作者头像 李华
网站建设 2026/9/7 9:39:26

无脚本自动化工作流:打通NAS、电脑与通讯平台的AI工具环境

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 9:36:18

嵌入式固件工程化:启动流程深度拆解与OTA升级实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华