JSON for Modern C++ 数值类型探秘:深入理解 number_integer_t 有符号整数类型
2026/9/8 18:30:39 网站建设 项目流程

JSON for Modern C++ 数值类型探秘:深入理解 number_integer_t 有符号整数类型

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

# JSON for Modern C++ 数值类型探秘:深入理解 number_integer_t 有符号整数类型

number_integer_t是 nlohmann/json(JSON for Modern C++)中用于存储JSON 有符号整数的核心类型别名。本文基于 number_integer_t 官方 API 文档 展开,结合仓库头文件源码与词法分析器实现,讲解它的定义方式、默认类型、边界限制、与number_unsigned_t/number_float_t的分工关系,以及整数在反序列化过程中的存储与溢出回退机制。读完本文,你将能够在自己的工程中准确判断整数值的容量边界、正确解释"超大整数自动降级为浮点"等行为,并掌握如何通过模板参数自定义该类型。

一、从 RFC 8259 说起:JSON 为什么需要三种数值类型

RFC 8259 对 JSON 数值(number)给出了定义式的描述:

数值的表示与大多数编程语言类似:使用十进制数字、以 10 为基数。一个数包含一个整数部分,可选地以负号开头,其后可以跟小数部分和/或指数部分。前导零是不允许的。(……)无法用下述文法表示的数值(如InfinityNaN)是不允许的。

这条定义实际上同时涵盖了整数浮点数两类数值。但 C++ 恰好提供了比"一个笼统的 number"更精确的存储能力——只要明确一个数到底是有符号整数无符号整数还是浮点数,就能做到既不浪费精度、也尽可能接近机器原生表示。因此,JSON for Modern C++ 没有沿用单一数值类型,而是把 JSON 数值拆成了三个相互独立、由basic_json模板参数驱动的类型别名:

类型别名模板参数存储的 JSON 数值文档
number_integer_tNumberIntegerType有符号整数本文
number_unsigned_tNumberUnsignedType无符号整数number_unsigned_t
number_float_tNumberFloatType浮点数number_float_t

在源码层面,这三个别名在basic_json类的"JSON value data types"段落中集中声明,见 include/nlohmann/json.hpp:

/// @brief a type for a number (integer) using number_integer_t = NumberIntegerType; // line 386 /// @brief a type for a number (unsigned) using number_unsigned_t = NumberUnsignedType; // line 390 /// @brief a type for a number (floating-point) using number_float_t = NumberFloatType; // line 394

也就是说,number_integer_t并不是一个独立设计的类,而是模板参数NumberIntegerType的直接别名

using number_integer_t = NumberIntegerType;

二、默认类型与模板参数的来源

basic_json类模板在 include/nlohmann/json_fwd.hpp 中完成前置声明,全部数值相关模板参数的默认值都收敛于此:

template< template<typename U, typename V, typename... Args> class ObjectType = std::map, template<typename U, typename... Args> class ArrayType = std::vector, class StringType = std::string, class BooleanType = bool, class NumberIntegerType = std::int64_t, // 有符号整数默认 std::int64_t class NumberUnsignedType = std::uint64_t, // 无符号整数默认 std::uint64_t class NumberFloatType = double, // 浮点数默认 double ... > class basic_json;

因此,在使用默认模板参数(即直接使用nlohmann::json这个特化别名)时,number_integer_t的默认值就是#!cpp std::int64_t

这也解释了为什么本文标题用到的官方示例能够以编译期方式验证这一点。仓库中的完整示例见 examples/number_integer_t.cpp:

#include <iostream> #include <iomanip> #include <nlohmann/json.hpp> using json = nlohmann::json; int main() { std::cout << std::boolalpha << std::is_same<std::int64_t, json::number_integer_t>::value << std::endl; }

其运行输出(number_integer_t.output)为:

true

std::is_same<std::int64_t, json::number_integer_t>::value在编译期判定json::number_integer_tstd::int64_t是否为同一类型。结果为true,即以默认配置编译时,整数存储单元就是 64 位有符号整数。

三、默认行为:前导零与八进制的"语言陷阱"

原文档特别提醒了一个极易踩坑的差异:C++ 并不强制 JSON 文法中"禁止前导零"的约束。JSON 字符串反序列化对前导零是严格报错的,但如果你在 C++ 源代码里直接写出带前导零的整数字面量,C++ 编译器会按八进制解释它。

举例来说,C++ 整数字面量010在语法层面等于十进制的 8,因此:

json j = 010; // C++ 层:八进制 010 == 十进制 8 std::cout << j.dump() << std::endl; // 输出:8

也就是说,内部保存的十进制值8在序列化(dump)时得到的是8而不是010。反方向再看反序列化:如果在 JSON 文本中写入带前导零的内容(例如{"a": 010}),解析阶段会直接抛出parse_error,这正是词法分析器严格遵循 RFC 8259 文法的结果。

从源码看,词法分析器 include/nlohmann/detail/input/lexer.hpp 中的scan_number()用一套显式状态机(scan_number_minusscan_number_zeroscan_number_any1scan_number_decimal1scan_number_exponentgoto标签)逐字符吞入数字令牌。scan_number_zero状态意味着"当前只读到了一个0",此时若紧接着读到的是数字字符,便会被判定为非法前导零而进入错误分支——这正是文档所述"反序列化时前导零产生错误"的实现根基。

四、整数容量边界:INT64_MIN 与 INT64_MAX

RFC 8259 允许实现方对数值的范围和精度施加限制:

An implementation may set limits on the range and precision of numbers.

默认使用std::int64_t时,JSON for Modern C++ 的有符号整数存储能力为:

项目
可存储的最大整数9223372036854775807(即INT64_MAX
可存储的最小整数-9223372036854775808(即INT64_MIN

两个边界上的注意事项:

  • 构造阶段:通过构造函数放入超出该区间的整数(例如直接用一个 128 位或字面量超界的值构造json),会触发有符号整数的溢出/下溢(over/underflow)行为。这一点沿用的是 C++ 整型本身的语义,使用时应当自行保证取值在[INT64_MIN, INT64_MAX]内。
  • 反序列化阶段:如果输入 JSON 中的整数字面量超出std::int64_t的表示范围,库不会报错丢弃,而是采用"就近降级"策略自动存储为其他两种类型:
    • 数值过大(超过INT64_MAX但可用uint64_t表示)→ 自动存储为number_unsigned_t
    • 数值进一步超界或形态上更适合 → 自动存储为number_float_t

上述机制在词法分析器 lexer.hpp 的scan_number_done阶段有清晰的代码证据:库"先尝试解析整数、解析失败再回退到浮点":

// try to parse integers first and fall back to floats if (number_type == token_type::value_unsigned) { const auto x = std::strtoull(token_buffer.data(), &endptr, 10); ... if (errno != ERANGE) { value_unsigned = static_cast<number_unsigned_t>(x); if (value_unsigned == x) { return token_type::value_unsigned; } } } else if (number_type == token_type::value_integer) { const auto x = std::strtoll(token_buffer.data(), &endptr, 10); ... if (errno != ERANGE) { value_integer = static_cast<number_integer_t>(x); if (value_integer == x) { return token_type::value_integer; } } } // this code is reached if we parse a floating-point number or if an // integer conversion above failed strtof(value_float, token_buffer.data(), &endptr); ... return token_type::value_float;

可以看到:无符号数尝试std::strtoull、有符号数尝试std::strtoll,一旦errno == ERANGE(溢出)或static_cast回绕后与原始值不等,代码就会落到末尾,把令牌交给strtof解析并以value_float令牌收尾——数值由此被归入浮点阵营。整套逻辑与官方文档"反序列化时太大或太小的整数会自动被存储为number_unsigned_tnumber_float_t"的描述完全吻合。

4.1 三种令牌与运行时类型

词法分析器用一组枚举令牌区分解析结果(见 lexer.hpp):

value_unsigned, ///< an unsigned integer -- use get_number_unsigned() for actual value value_integer, ///< a signed integer -- use get_number_integer() for actual value value_float, ///< a floating point number -- use get_number_float() for actual value

分别对应解析器(parser)随后调用get_number_unsigned()get_number_integer()get_number_float()取值,再落到value_t::number_unsignedvalue_t::number_integervalue_t::number_float三个运行时类型标签之一。

五、互操作区间:[-2^53+1, 2^53-1]

RFC 8259 还给出了关于"跨实现一致性"的指导性说明:

Note that when such software is used, numbers that are integers and are in the range $[-2^{53}+1, 2^{53}-1]$ are interoperable in the sense that implementations will agree exactly on their numeric values.

[-2^53+1, 2^53-1]这个区间之所以存在,是因为大量 JSON 实现(尤其基于 JavaScript / IEEE 754 双精度浮点的实现)只能精确表示该范围内的整数。而 JSON for Modern C++ 的默认有符号整数类型std::int64_t的精确表示区间是[INT64_MIN, INT64_MAX]

[-2^53+1, 2^53-1] ⊂ [INT64_MIN, INT64_MAX]

互操作区间是自身精确支持区间的真子集,因此可以得出两个结论:

  1. 凡是落在[-2^53+1, 2^53-1]内的整数,本库都能与其他遵守该约定的实现逐位精确一致地解析与序列化,不会出现精度丢失;
  2. 超出该子集、但仍在std::int64_t范围内的整数,本库仍然能无损保存,只是在与基于双精度浮点的其他系统交换时,对方可能无法精确表示——这一点是设计上的安全余量,而非缺陷。

六、存储方式:直接内嵌的联合体成员

JSON for Modern C++ 在值存储上采取"类型标签 + 联合体"的紧凑布局。整数(以及其他数值、布尔、字符串指针)被直接存放在一个匿名union中,见 include/nlohmann/json.hpp:

union json_value { number_integer_t number_integer; // 有符号整数成员,即 number_integer_t number_unsigned_t number_unsigned; number_float_t number_float; boolean_t boolean; string_t* string; object_t* object; array_t* array; ... json_value(number_integer_t v) noexcept : number_integer(v) {} ... };

结合本节之前的讨论可以总结出"直接存储"的两层含义:

  • 布局上number_integer_t类型的值就是json_value联合体中的一个原生成员,一个json对象对整数值不存在二级堆分配或间接指针,读写都落在联合体自身的内存上;
  • 语义上:运行类型由m_data.m_type(取自value_t枚举)与联合体成员共同决定。例如is_number_integer()的判定实现(include/nlohmann/json.hpp)同时涵盖了有符号与无符号两种标签:
constexpr bool is_number_integer() const noexcept { return m_data.m_type == value_t::number_integer || m_data.m_type == value_t::number_unsigned; }

这提示读者:在JSON for Modern C++中,is_number_integer()的语义更接近"这是一个整型数值(不分正负)",而无符号整数值同样会被视为整型;若要严格区分正负,需要结合is_number_unsigned()一起判断。

七、自定义 NumberIntegerType 与使用建议

既然number_integer_t完全由basic_json的模板参数NumberIntegerType决定,那么当项目对整数范围有特殊要求时,可以通过特化basic_json替换它。例如:

#include <nlohmann/json.hpp> #include <cstdint> // 使用 __int128 等扩展类型前,务必确认目标编译器的支持情况 // 下面以常见的“改用 32 位有符号整数”为例,示意特化方法: template< template<typename U, typename V, typename... Args> class ObjectType = std::map, template<typename U, typename... Args> class ArrayType = std::vector, class StringType = std::string, class BooleanType = bool, class NumberIntegerType = std::int32_t, // 关键:替换有符号整数类型 class NumberUnsignedType = std::uint32_t, class NumberFloatType = double, template<typename U> class AllocatorType = std::allocator> using my_json = nlohmann::basic_json< ObjectType, ArrayType, StringType, BooleanType, NumberIntegerType, NumberUnsignedType, NumberFloatType, AllocatorType>; static_assert(std::is_same<my_json<>::number_integer_t, std::int32_t>::value, "custom integer type applied");

需要特别强调的是:

  • 改动NumberIntegerType时,应同时留意配套的NumberUnsignedType,确保两者表示能力匹配(参考前文默认的int64_t/uint64_t对称设计);
  • number_integer_t的类型变化会影响整库的算术、比较与序列化路径,属于全局性 ABI 级调整,一般仅在确有跨端整型协议对齐需求时才建议特化;
  • 多数场景下,直接使用默认的std::int64_t并遵循前文的范围约束即可满足要求。

八、版本与适用范围

number_integer_t自 JSON for Modern C++1.0.0版本起便已提供,属于该库最基础的数值承载类型之一,其语义在历代版本中保持稳定。本文所有源码证据均来自仓库当前 3.12.0 版本,读者若使用不同大版本,建议以对应版本的 single_include/nlohmann/json.hpp(单头文件形态)或 include/nlohmann/json.hpp(分模块形态)中的实际声明为准。

小结

number_integer_t用一句using number_integer_t = NumberIntegerType;概括了 JSON for Modern C++ 对有符号整数的全部设计哲学:以模板参数注入类型、以std::int64_t为默认、以"整数优先 + 浮点回退"的词法策略兜底超界输入,并以联合体直接内嵌的方式零开销存储。理解它的默认值与边界行为,是准确预估json对象内存布局、排查"大整数变浮点"类精度问题的第一步,也是进一步阅读number_unsigned_tnumber_float_t两篇姊妹文档的基础。

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

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询