JSON for Modern C++ 数值类型检查:basic_json::is_number_float() 语义、用法与实现解析
is_number_float() 是 nlohmann/json(JSON for Modern C++) 中 basic_json 类提供的一组"类型检查"(type checker)成员函数之一,用于精确判断一个 JSON 值是否存储为浮点数(floating-point number)。本文以 官方 API 文档 is_number_float.md 为骨架,结合该仓库内 json.hpp、value_t.hpp 的源码实现与 unit-inspection.cpp 测试用例,完整讲解该函数的签名语义、返回值、异常与复杂度保证、全类型示例、底层判别原理,以及它与 is_number()、is_number_integer()、is_number_unsigned() 之间的分工关系。读完本文,你将掌握"如何在运行时无歧义地区分 JSON 中的整数与浮点数值",并能在解析用户输入或二次序列化前正确使用这些检查函数。
函数签名与核心语义
该函数的完整声明位于 include/nlohmann/json.hpp 中 basic_json 类的公开接口区,官方文档给出的形式为:
constexpr bool is_number_float() const noexcept;
其语义是:当且仅当该 JSON 值是一个浮点数时返回 #!cpp true。这里的"浮点数"在类型层面是严格的——它排除了有符号整数与无符号整数,即便某个整数在数值上恰好等于某个可精确表示的浮点数,只要它以整数形式存储,is_number_float() 依然返回 false。反之,形如 23.42、1e3、-0.5 这类带小数部分或指数部分的数值在解析后归属浮点存储类别,函数返回 true。
与其他数值类型检查函数的分工
该函数是 nlohmann/json"数值三分类"检查体系的一员,与之并列的是:
- is_number_integer():返回
#!cpp true当且仅当值是(有符号或无符号)整数; - is_number_unsigned():返回
#!cpp true当且仅当值是无符号整数; - is_number():返回
#!cpp true当且仅当值是任一类型的数字,即整数或浮点数的并集。
它们各自回答一个不同的问题,组合起来可构成完整的判别矩阵:
| JSON 值示例(存储方式) | is_number() |
is_number_integer() |
is_number_unsigned() |
is_number_float() |
|---|---|---|---|---|
有符号整数(如 17) |
true |
true |
false |
false |
无符号整数(如 12345678987654321u) |
true |
true |
true |
false |
浮点数(如 23.42) |
true |
false |
false |
true |
| 字符串 / 数组 / 对象 / null / 布尔 / 二进制 | false |
false |
false |
false |
在 is_number.md 文档 中甚至直接给出了 is_number() 的"可行实现"——return is_number_integer() || is_number_float();,这恰好印证了三者之间的逻辑关系。
返回值、异常安全与复杂度保证
官方文档对本函数的三个契约性保证如下,需逐一理解其工程含义:
- 返回值:类型为浮点数时返回
#!cpp true,否则返回#!cpp false; - 异常安全:No-throw guarantee(不抛出任何异常),函数声明中携带的
noexcept关键字即编译期契约; - 复杂度:常数级(Constant)。
这三点之所以成立,原因在于该函数的实现只是"比较一个类型标签",不涉及内存分配、迭代或用户代码调用。这在下面"底层实现"一节将得到源码级验证。得益于 constexpr 与 noexcept 的双重修饰,该函数不仅可在运行时安全检查,理论上也具备在常量表达式(如 static_assert)中使用的前提(在满足 C++ 标准的 constexpr 求值条件下)。
覆盖全部 JSON 类型的完整示例
官方文档对应的可编译示例位于 examples/is_number_float.cpp,它一次性覆盖了 nlohmann/json 支持的全部九类值:null、布尔、整数、无符号整数、浮点数、对象、数组、字符串与二进制数据。示例源码如下:
#include <iostream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
// create JSON values
json j_null;
json j_boolean = true;
json j_number_integer = 17;
json j_number_unsigned_integer = 12345678987654321u;
json j_number_float = 23.42;
json j_object = {{"one", 1}, {"two", 2}};
json j_array = {1, 2, 4, 8, 16};
json j_string = "Hello, world";
json j_binary = json::binary({1, 2, 3});
// call is_number_float()
std::cout << std::boolalpha;
std::cout << j_null.is_number_float() << '\n';
std::cout << j_boolean.is_number_float() << '\n';
std::cout << j_number_integer.is_number_float() << '\n';
std::cout << j_number_unsigned_integer.is_number_float() << '\n';
std::cout << j_number_float.is_number_float() << '\n';
std::cout << j_object.is_number_float() << '\n';
std::cout << j_array.is_number_float() << '\n';
std::cout << j_string.is_number_float() << '\n';
std::cout << j_binary.is_number_float() << '\n';
}
程序输出(见 is_number_float.output):
false
false
false
false
true
false
false
false
false
注意第 4 行:12345678987654321u 虽然数值巨大,但因其带 u 后缀而被构造为无符号整数存储,因此 is_number_float() 依旧返回 false——再一次印证了该函数判别的是存储类型而非数值大小。
底层实现:类型标签比较驱动的 constexpr 判定
is_number_float() 的实现极简,直接位于 include/nlohmann/json.hpp#L1414-L1419:
constexpr bool is_number_float() const noexcept
{
return m_data.m_type == value_t::number_float;
}
其本质是一次 value_t 枚举标签的相等比较。value_t 是库内部用于区分不同 JSON 类型(null、object、array、string、boolean、number_integer、number_unsigned、number_float、binary、discarded)的核心枚举,定义于 include/nlohmann/detail/value_t.hpp#L53-L65。与之对应,同一处源码还展示了姊妹函数的实现方式:
constexpr bool is_number() const noexcept // m_data.m_type == ... number_integer/unsigned/float 的并
{
return is_number_integer() || is_number_float();
}
constexpr bool is_number_integer() const noexcept // number_integer || number_unsigned
{
return m_data.m_type == value_t::number_integer || m_data.m_type == value_t::number_unsigned;
}
constexpr bool is_number_unsigned() const noexcept // 仅 number_unsigned
{
return m_data.m_type == value_t::number_unsigned;
}
由此可清晰理解:nlohmann/json 并非简单地将 JSON 数字一律映射为 double,而是为了让 C++ 侧能够"更精确地存储",把数值细分为三个枚举成员(有符号整数、无符号整数、浮点数)。这种设计正是 is_number_integer()/is_number_unsigned()/is_number_float() 三函数并存的根本原因,其设计动机在 value_t.hpp 的注释中有明确说明。
解析阶段浮点类别是如何被"打标"的
浮点类型标签并非凭空产生,而是在解析数字字面量时由词法分析器(lexer)决定。nlohmann/json 的词法扫描函数 scan_number()(见 include/nlohmann/detail/input/lexer.hpp,起始于约第 1002 行)实现了一个基于 goto 的状态机。状态转移表(约第 977–981 行)表明:当扫描到小数点或 e/E 指数符号时,数字会转入 decimal/exponent 相关状态,从而不可能再归类为纯整数 token。
在扫描完成的收尾阶段(lexer.hpp#L1277-L1327),代码先尝试用 std::strtoull/std::strtoll 把 token 解析为无符号/有符号整数,仅当解析溢出(errno == ERANGE)或 token 本身带小数点/指数时,才回退到 strtof(库内高性能浮点解析)并返回 token_type::value_float:
// 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;
从源码结构看,这意味着两个推论:其一,形如 1.0、2e3 的 JSON 文本即使在数学上是整值,只要字面上含小数点或指数就会被标为浮点;其二,超出 std::int64_t/std::uint64_t 表示范围的大整数也会降级为浮点存储,从而被 is_number_float() 判为 true。这些行为构成了"同一份 JSON 数据在不同写法下可能落入不同类型标签"的边界情形,也正是类型检查函数需要存在的原因。
浮点底层存储类型 number_float_t 与边界行为
浮点数值在库中经由模板参数 NumberFloatType 抽象出的别名 number_float_t 存储(详见 number_float_t.md 文档 与 json.hpp#L393-L394 处 using number_float_t = NumberFloatType;)。默认情况下:
NumberFloatType默认为double,因此number_float_t默认为 IEEE-754 双精度类型;- 超出
double表示范围(小于-1.79769313486232e+308或大于1.79769313486232e+308)的数值在内部被存储为 NaN,并在序列化时输出为null; - NaN(非数值)同样会被序列化为
null;序列化逻辑中对 NaN 的规避可在输出模块 serializer.hpp 的dump_float系列函数中看到依据。
理解这一点对正确使用 is_number_float() 很关键:该函数只能回答"当前是否按浮点存储",不能保证该浮点值一定来自合法的有限 JSON 数字——极端越界数值会以 NaN 形式存在,此时配合数值检查与范围校验才安全。
测试佐证:完整的类型判别矩阵
类型检查语义在测试套件中被逐类型验证。核心测试文件 tests/src/unit-inspection.cpp 中针对 object、array、null、boolean、integer、unsigned integer、floating-point、string、binary、discarded 等每种值分别断言了全部类型检查函数的结果。其中浮点分支(约第 148–164 行):
SECTION("number (floating-point)")
{
json const j(42.23);
...
CHECK(j.is_number());
CHECK(!j.is_number_integer());
CHECK(!j.is_number_unsigned());
CHECK(j.is_number_float());
...
}
对应地,对象/数组/null/布尔等分支中都以 CHECK(!j.is_number_float()) 断言其反例(如第 30、48、66 行等)。此外,unit-regression1.cpp(约第 153–171、677–683、897–903、1096、1191 行)在回归场景中反复使用 is_number_float() 校验浮点解析结果的正确归属,unit-ubjson.cpp(第 804 行)与 unit-bjdata.cpp(第 1332 行)则在二进制格式(UBJSON/BJData)往返解析后再次断言值仍保持浮点类别。这说明该函数是序列化、反序列化全链路中稳定可靠的"类型探针"。
典型实战场景
综合前述语义与实现,is_number_float() 最常见的落地场景包括:
- 输入校验与分流:解析外部 JSON 后,先用
is_number_float()/is_number_integer()区分数值形态,再决定按整数还是浮点处理,避免把23.42截断成整数导致数据损坏; - 格式化策略选择:需要自定义序列化时,先判断
is_number_float()再选择"保留小数位/指数形式"的输出路径,与 serializer.hpp 内部对浮点与整数分别走dump_float与整数路径的做法一致; - 数据迁移与类型审计:遍历 JSON 树时用
is_number_float()找出所有浮点字段,配合number_float_t的精度边界评估(如金额、ID 等应使用整数存储的字段)是否会在二次处理中发生精度漂移; - 与
get<T>()/指针访问协同:在调用get<number_float_t>()或get_ptr<number_float_t>()等取值操作前先做类型守卫,规避类型不匹配导致的行为差异。
版本沿革
根据官方文档的 version history:is_number_float() 自 1.0.0 版本起即已提供。与之相对,is_number() 在 2.0.0 版本扩展为同时对无符号整数返回 true。这一沿革提醒使用者:在涉及跨版本兼容的历史代码中,"数字"判定的口径曾发生过演进,而精确到 is_number_float() 这一层的判定语义则始终稳定,适合作为不依赖版本差异的可靠判断依据。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00