
JSON for Modern C 诊断位置详解nlohmann::basic_json::end_pos 与 JSON_DIAGNOSTIC_POSITIONS 实战【免费下载链接】jsonJSON for Modern C项目地址: https://gitcode.com/GitHub_Trending/js/jsonend_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结尾、null以l结尾所以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 子目录、FetchContent或find_package方式集成时可以不用手动定义宏而是使用 CMake 选项。项目根 CMakeLists.txt 中声明了该选项默认OFFoption(JSON_Diagnostic_Positions Enable diagnostic positions. OFF)开启后该选项通过target_compile_definitions给nlohmann_json目标追加JSON_DIAGNOSTIC_POSITIONS1的接口级定义见 CMakeLists.txt 中的$$BOOL:${JSON_Diagnostic_Positions}:JSON_DIAGNOSTIC_POSITIONS1。配置文档 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_PositionsON # 子项目场景下对应 nlohmann_json 的选项代价说明启用该宏会带来额外开销官方文档见 JSON_DIAGNOSTIC_POSITIONS明确指出每个 JSON 值会多出两个std::size_t成员start_position与end_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.outputRoot 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_array用get_position() - 1记录[handle_end_array记录]之后的位置原始值布尔、null、字符串、数字handle_diagnostic_positions_for_json_value先用当前游标写入end_position再按值的类型反推start_position。从源码结构看其策略是按已知字面量长度倒推boolean减去 4true或 5falsenull减去 4string/数字等则减去词法单元文本长度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 中强调的两条约束只有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) { /* 可安全使用区间 */ }位置失效Invalidation警告返回的位置仅在 JSON 值不被修改时有效值被修改后位置不会随之更新。从源码看库并不在每次operator[]/push_back等变更操作后重新计算位置那既昂贵也做不到因为增量修改没有统一的源串偏移概念。所以正确姿势是解析后只读地查询位置一旦要写回源文本应基于解析时的快照字符串 位置区间做拼接或替换而不是期待库自动维护一致性。位置是相对于被解析的那份输入字符串而非 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()本身只是一个constexpr、noexcept、常数时间的简单访问器但其背后是一套以词法游标为基准、在 SAX 解析回调中逐节点写入的位置追踪机制见 json_sax.hpp。实际使用时的要点开启前提编译前定义JSON_DIAGNOSTIC_POSITIONS 1或使用 CMake 选项JSON_Diagnostic_PositionsON默认均为关闭语义半开区间右端点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),仅供参考