现代 C++ 中的声明式 JSON 分发

发布: (2025年12月20日 GMT+8 03:34)
8 min read
原文: Dev.to

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:一个值恰好是互斥的 nullbooleannumberstringarrayobject 其中之一。
  • 当前痛点
    • 条件链随着案例的增多而难以扩展。
    • 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_typehas_field 等)
绑定隐式、分散bind() 使其显式化
行为与匹配逻辑混合处理器(print_nullhandle_array 等)是纯粹的动作

匹配表达式本身是 声明式描述,用于描述调度策略,而不是一系列分支的编码实现。

设计约束

  1. 无隐式绑定 – 所有需要数据的地方必须通过 bind() 显式绑定。
  2. 三步分离
    • 匹配 – 确定案例是否适用。
    • 绑定 – 捕获将传递给处理器的数据。
    • 处理 – 执行副作用(输出、递归等)。

守卫在 绑定之后 进行评估,确保谓词在具体值而非符号占位符上运行。因此,处理器签名保持稳定且可预测:每个处理器仅接收一个参数 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_objectprint_intprint_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

Back to Blog

相关文章

阅读更多 »

SOLID 再探 — 后模式视角

为什么原则不如背后的力量重要:SOLID 不是一份检查清单,而是对更深层力量的历史压缩。这是系列的第 5 部分。