现代 C++ 中的声明式 JSON 分发
Source: Dev.to
请提供您希望翻译的正文内容,我将为您翻译成简体中文并保留原有的格式、Markdown 语法以及技术术语。谢谢!
Working with JSON in C++
“在 C++ 中处理 JSON 已不再是单纯的字节解析问题。像 nlohmann/json 这样的成熟库已经提供了对 JSON 值的强大、动态的表示。真正的挑战出现在解析之上的一层:如何组织解释这些值的控制流。”
对于小例子,线性的 if / else if 检查链通常已经足够。可是,一旦逻辑变得递归或语义上更为细致——例如,区分空数组与非空数组,或识别具有特定结构属性的对象——代码就会开始积累大量的分支噪声。分类、分支和行为紧密耦合,使得局部推理控制流变得困难。
Goal
“与其询问如何优化条件逻辑,不如问一下调度本身是否可以用声明式方式描述,同时仍然在现有的动态 JSON 模型上操作。”
完整实现请参见
🔗
以下仅展示关键片段。
为什么使用声明式分发?
- Dynamic JSON ≈ Sum Type – 虽然
nlohmann::json是动态类型的,但其运行时行为实际上等同于 sum type:一个值恰好是互斥的 null、boolean、number、string、array 或 object 其中之一。 - 当前痛点
- 条件链随着案例的增多而难以扩展。
- Visitor‑style 设计引入间接性和样板代码,却未能解决匹配与行为之间的语义耦合问题。
- Variant‑based 分发要求将 JSON 转换为封闭的代数数据类型,这在实际中往往不可行。
我们需要一种 直接描述分发逻辑 的方式,将其视为语义案例的组合,而不替换底层数据模型。
Source: …
匹配‑管道方法
void parse_json(const json& j, int depth = 0) {
match(j)
.when(bind()[is_type(json::value_t::null)] >> print_null(depth))
.when(bind()[is_type(json::value_t::boolean)] >> print_bool(depth))
.when(bind()[is_int] >> print_int(depth))
.when(bind()[is_uint] >> print_uint(depth))
.when(bind()[is_float] >> print_float(depth))
.when(bind()[is_type(json::value_t::string)] >> print_string(depth))
.when(bind()[is_type(json::value_t::array)] >> handle_array(depth))
.when(bind()[has_field("name")] >> handle_named_object(depth))
.otherwise([=] {
indent(depth);
std::cout << "\n";
});
}
这给我们带来了什么
| 关注点 | 传统代码 | 声明式匹配 |
|---|---|---|
| 分类 | 与控制流交织在一起 | 显式谓词(is_type、has_field 等) |
| 绑定 | 隐式、分散 | bind() 使其显式化 |
| 行为 | 与匹配逻辑混合 | 处理器(print_null、handle_array 等)是纯粹的动作 |
匹配表达式本身是 声明式描述,用于描述调度策略,而不是一系列分支的编码实现。
设计约束
- 无隐式绑定 – 所有需要数据的地方必须通过
bind()显式绑定。 - 三步分离
- 匹配 – 确定案例是否适用。
- 绑定 – 捕获将传递给处理器的数据。
- 处理 – 执行副作用(输出、递归等)。
守卫在 绑定之后 进行评估,确保谓词在具体值而非符号占位符上运行。因此,处理器签名保持稳定且可预测:每个处理器仅接收一个参数 const json&,不论匹配条件多么复杂。
Predicates
inline auto is_type(json::value_t t) {
return [=](const json& j) { return j.type() == t; };
}
inline auto has_field(const std::string& key) {
return [=](const json& j) {
return j.is_object() && j.contains(key);
};
}
在 match 表达式中,这些谓词承担的是 语义角色,而不是控制流角色。guard 用来细化 case 的含义;它 不 代表可执行的分支。随着谓词变得更加复杂或在多个 case 中被复用,这一区别变得尤为关键。
Handlers – Pure Behaviour
inline auto print_null(int depth) {
return [=](auto&&...) {
indent(depth);
std::cout << "null\n";
};
}
处理器的签名反映了它的意图:它响应的是语义类别,而不是特定的数据形状。
这种解耦使得处理器可以独立于匹配结构演进,并且明确系统的哪一部分负责哪项决策。
递归遍历
inline auto handle_array(int depth) {
return [=](const json& j) {
indent(depth);
std::cout << "[\n";
for (const auto& elem : j) {
parse_json(elem, depth + 1);
}
indent(depth);
std::cout << "]\n";
};
}
(其他处理函数,如 handle_named_object、print_int、print_string 等,遵循相同的模式。)
摘要
- 声明式匹配管道 让你描述每种 JSON 结构 要做什么,而不是 如何实现。
- 通过将 匹配、绑定 和 处理 分离,代码保持可读、可扩展,并且避免了纠结的控制流结构。
- 该方法直接作用于动态的
nlohmann::json模型,避免了将 JSON 转换为封闭代数数据类型的需求。
欢迎在链接的仓库中查看完整的示例实现,祝你调度愉快!
inline auto handle_array_verbose(int depth) {
return [=](const json& arr) {
indent(depth);
std::cout << "array [" << arr.size() << "]\n";
for (const auto& v : arr) {
parse_json(v, depth + 1);
}
};
}
递归是显式的、局部的且不令人意外。没有隐藏的控制流,也没有遍历与分发策略之间的隐式耦合。
这种方法的优势 不在于 语法新颖,而在于它所强制的纪律:
- 分类局限于模式。
- 语义约束以守卫的形式表达。
- 行为被限制在处理器中。
随着新案例的加入——额外的结构区分、类似模式的检查或专门的解释——匹配表达式会 横向 扩展,而不是变得更深层嵌套。
一个有趣的扩展是 提取器模式:一种直接验证结构并绑定子组件的模式。在 JSON 场景下,这种模式可以匹配具有特定字段的对象,并将这些字段绑定为独立的值,从而把结构分解从处理器移到模式层本身。
完整实现(包括所有谓词和处理器)可在以下地址获取
https://github.com/sentomk/patternia/tree/main/samples/json_dispatch。