欢迎光临
我们一直在努力

【C++三方组件】RapidJSON 上:API 分类精讲

【C++三方组件】RapidJSON 上:API 分类精讲

【摘要】:上一篇说过:性能敏感的热路径,从 nlohmann/json 换到 RapidJSON。但 RapidJSON 是一辆手动挡——分配器要自己递、类型要自己查、字符串默认不拷贝,API 面也比 nlohmann 宽一倍。本文按类精讲它的六类 API:解析(含 ParseFlags 全参数表)、错误处理、DOM 查询、DOM 修改(重点讲清拷贝与引用的分界)、序列化(Writer/PrettyWriter)、SAX(Handler 十三个回调),外加原位解析一节,每类配参数说明、示例代码与实测输出;末尾是与 nlohmann 的同任务写法对照表和六个必知的坑。下一篇拆它的性能实现。 【关键词】:RapidJSON、JSON、DOM、SAX、分配器、原位解析、序列化 【版本基准】:RapidJSON 1.1.0(master@24b5e7a,2024-12,MIT)|C++17|文中输出均为 g++ 11.2 实测

1. 开场:从自动挡换到手动挡

同一个「给对象加个成员」,两种世界观:

// nlohmann/json:自动挡
j["port"] = 8080;

// RapidJSON:手动挡
doc.AddMember("port", 8080, doc.GetAllocator());

自动挡舒服——类型、内存、拷贝全自动;代价藏在热路径上:一遍解析数十万次时,每个字符串一次堆分配、每次类型判断一层抽象,账单会说话。RapidJSON 的选择是把所有自动的东西交还给你:分配器显式传递、类型显式检查、字符串显式选择拷不拷。手动挡开好了,就是本专栏速度梯队里前排的那辆车。

本文只讲怎么开;它为什么快,留给下篇。所有示例可编译(g++ -std=c++17 -I<rapidjson/include>),输出为实测。

2. 30 秒接入

纯头文件库,接入三选一:

// vcpkg:vcpkg.json
{ "dependencies": [ "rapidjson" ] }

# vcpkg / 系统安装通用
find_package(RapidJSON CONFIG REQUIRED)
target_link_libraries(app PRIVATE rapidjson)

# 或 FetchContent(header-only,URL 即锁版本)
include(FetchContent)
FetchContent_Declare(rapidjson
GIT_REPOSITORY https://github.com/Tencent/rapidjson
GIT_TAG v1.1.0)
FetchContent_MakeAvailable(rapidjson)

最小验证一行:rapidjson::Document d; d.Parse("{\\"ok\\":true}");。

3. 心智模型:手动挡三定律

Value(GenericValue 的 UTF8 特化)是一个「类型位标记 + 紧凑联合体」的值,七种类型:

类型判断取值构造
null IsNull() —— Value()
false / true IsBool() GetBool() Value(false)
object IsObject() GetObject() Value(kObjectType)
array IsArray() GetArray() Value(kArrayType)
string IsString() GetString() Value("s")(引用)
number IsInt() 等六兄弟 GetInt() 等 Value(42)

Document 是特殊的 Value,自带内存池与解析器。开好这辆手动挡,记住三定律,后面六节全是三定律的展开:

  • 先 Is 后 Get:取值前判断类型;GetInt() 不检查,类型错了 release 下是未定义行为;
  • 修改必递分配器:一切会生长的操作(AddMember/PushBack/拷贝字符串)都要传 Allocator&;
  • 字符串默认引用不拷贝:要不要拷贝,你说了算——这也是最大的坑(§11)。
  • 4. 解析类 API:Parse 家族与 ParseFlags

    API参数说明
    Parse(const Ch* str) 零终止字符串 默认 flags 解析
    Parse(const Ch* str, size_t length) 带长度 可含 \\0
    ParseStream(InputStream& is) StringStream(内存)/ FileReadStream(文件)
    ParseInsitu(Ch* str) 可写缓冲 in-situ 解析,字符串零拷贝(§10)
    Parse<parseFlags>(…) 模板参数 组合下表任意 flags

    parseFlags 是模板位标志,取值与含义(源码 reader.h:148 起,值即位序):

    flag值含义
    kParseInsituFlag 1 原位(破坏性)解析,字符串零拷贝
    kParseValidateEncodingFlag 2 校验字符串编码
    kParseIterativeFlag 4 迭代式解析,函数栈 O(1),防深嵌套爆栈
    kParseStopWhenDoneFlag 8 读到一个完整根值即停(一个流里多个 JSON 时用)
    kParseFullPrecisionFlag 16 数字全精度解析(慢;默认最多 3 ULP 误差)
    kParseCommentsFlag 32 允许 // 与 /* */ 注释
    kParseNumbersAsStringsFlag 64 数字保留为字符串,不转数值
    kParseTrailingCommasFlag 128 允许尾逗号
    kParseNanAndInfFlag 256 允许 NaN/Inf/Infinity
    kParseEscapedApostropheFlag 512 允许 \\'

    using namespace rapidjson;

    Document doc;
    doc.Parse(R"({
    "name": "asset-sync", "retries": 3,
    "ratio": 0.75, "active": true,
    "tags": ["fast", "safe"],
    "server": {"host": "127.0.0.1", "port": 8080}
    })"
    );
    // 成功:doc.IsObject() == true

    5. 错误处理类 API:错误码 + 字节偏移

    RapidJSON 不抛异常(默认),错误三件套挂在 Document 上:

    API返回说明
    HasParseError() bool 是否出错
    GetParseError() ParseErrorCode 错误码枚举
    GetErrorOffset() size_t 出错处的字节偏移(从 0 起)
    GetParseError_En(code) const char* 错误码 → 英文描述(error/en.h)

    错误码枚举覆盖 20 种语法错误(error/en.h 可查全表):文档级(kParseErrorDocumentEmpty)、对象级(缺名/缺冒号/缺逗号)、字符串级(非法转义/坏代理对/缺引号)、数字级(太大/缺小数部分)等。实测一个尾逗号:

    Document d;
    d.Parse("{\\"a\\":1,}"); // JSON 标准不允许尾逗号
    std::cout << GetParseError_En(d.GetParseError())
    << " @" << d.GetErrorOffset() << "\\n";

    实测输出:

    Missing a name for object member. @7

    偏移 7 正是 } 的位置——拿到偏移直接定位到字节,比「解析失败」四个字有用得多。要容忍尾逗号,Parse<kParseTrailingCommasFlag> 一行解决。

    6. DOM 查询类 API:先 Is 后 Get

    API说明
    IsInt() IsUint() IsInt64() IsUint64() IsDouble() IsNumber() 数字六态判断
    GetInt() GetDouble() GetString() GetBool() 取值(不检查,见 §11 坑 ①)
    GetStringLength() 字符串长度(可含 \\0,别用 strlen)
    HasMember(name) 有无此键(线性查找)
    FindMember(name) 返回迭代器,找不到为 MemberEnd()
    d["name"] 语法糖,缺键时断言/UB
    MemberBegin()/MemberEnd() 成员迭代器
    Size() Empty() Begin()/End() 数组配套
    GetObject() GetArray() C++17 风格范围(支持 range-for 与结构化绑定)

    assert(doc.IsObject() && doc["retries"].IsInt());
    std::cout << doc["name"].GetString() << " "
    << doc["retries"].GetInt() << " "
    << doc["ratio"].GetDouble() << " "
    << doc["active"].GetBool() << "\\n";

    std::cout << "has=" << doc.HasMember("tags")
    << " miss=" << (doc.FindMember("ghost")
    == doc.MemberEnd()) << "\\n";

    const Value& tags = doc["tags"]; // 数组
    std::cout << "tags[" << tags.Size() << "]: ";
    for (auto& t : tags.GetArray())
    std::cout << t.GetString() << " ";
    std::cout << "\\n";

    for (auto& m : doc.GetObject()) { // C++17 范围式
    if (m.value.IsNumber())
    std::cout << m.name.GetString() << " ";
    }
    std::cout << "\\n";

    实测输出:

    asset-sync 3 0.75 1
    has=1 miss=1
    tags[2]: fast safe
    retries ratio

    注意最后一行:数字值的成员按插入序输出(retries 在 ratio 前)。RapidJSON 的 object 是成员数组不是 std::map——遍历序 = 写入序,这与 nlohmann/json 默认的字典序正好相反(那边要用 ordered_json 才有此待遇)。代价是 FindMember 线性查找,键很多时它不是哈希表的对手。

    7. DOM 修改类 API:分配器贯穿全程

    API说明
    SetObject() SetArray() SetNull() SetInt() 重置类型
    AddMember(name, value, alloc) 加成员;键是 StringRefType
    PushBack(value, alloc) / PopBack() 数组头尾操作
    EraseMember(name) / Erase(begin,end) 删除
    SetString(…) 见下方拷贝语义表
    Value(…).Move() 显式移交所有权

    拷贝语义表——手动挡的核心档位:

    写法键字符串值适用
    AddMember("k", 42, a) 引用 ——(数字) 字面量键 + 基本类型值
    AddMember("k", "lit", a) 引用 引用 键与值都是长寿字面量
    SetString(s) / SetString(StringRef(s)) —— 引用 缓冲区与 Document 同寿
    SetString(s, len, a) —— 拷贝 临时缓冲区必须拷
    v.SetString(buf, len, a); AddMember("k", v, a) 引用 拷贝 通用安全写法

    Document d2(kObjectType);
    auto& a = d2.GetAllocator(); // 分配器贯穿全程

    d2.AddMember("name", "demo", a); // 字面量:引用,安全
    d2.AddMember("port", 8080, a);

    Value arr(kArrayType);
    for (int i = 1; i <= 3; ++i)
    arr.PushBack(i, a); // 数组生长也要递分配器
    d2.AddMember("nums", arr, a); // arr 所有权移交 d2

    Value tmp(kStringType);
    char buf[] = "temp buffer"; // 栈上临时缓冲
    tmp.SetString(buf, sizeof(buf)1, a); // 显式拷贝
    d2.AddMember("tmp", tmp, a);

    AddMember("nums", arr, a) 之后 arr 变成 null——这是移动语义,不是拷贝;继续用 arr 是未定义行为。C++11 后也可以用右值直接写 AddMember("nums", Value(arr).Move(), a)。

    8. 序列化类 API:Writer 一趟直出

    API说明
    Writer<OutputStream> 紧凑输出,无多余空白
    PrettyWriter<OutputStream> 缩进美化(SetIndent 可调)
    StringBuffer 内存输出流(GetString() 取结果)
    value.Accept(writer) 把 DOM 写到 writer
    FileWriteStream 文件输出流(配 FileWrapper 用)

    StringBuffer sb;
    Writer<StringBuffer> w(sb);
    d2.Accept(w);
    std::cout << sb.GetString() << "\\n";

    StringBuffer sb2;
    PrettyWriter<StringBuffer> pw(sb2);
    d2.Accept(pw);
    std::cout << sb2.GetString() << "\\n";

    实测输出(节选):

    {"name":"demo","port":8080,"nums":[1,2,3],"tmp":"temp buffer"}
    {
    "name": "demo",
    "port": 8080,

    }

    Writer 也是一个 Handler——SAX 事件直通输出,中途不建 DOM。「SAX 解析 → Writer 输出」可以拼成一条零 DOM 的转换管道:过滤字段、改键名、缩进重排,全程内存 O(1)。

    9. SAX 类 API:十三个回调读完就走

    Reader::Parse(stream, handler) 把解析事件同步推给 handler。Handler 不是基类,是一个满足以下签名的鸭子类型(concept):

    回调参数时机
    Null() —— 读到 null
    Bool(b) bool 布尔值
    Int(i) / Uint(u) / Int64 / Uint64 / Double(d) 各整数类型 数字(解析器已按值域分流)
    String(s, len, copy) const Ch*, SizeType, bool 字符串
    RawNumber(s, len, copy) 同上 仅 kParseNumbersAsStringsFlag 时
    StartObject() / EndObject(n) 成员数 对象起止
    Key(s, len, copy) 同 String
    StartArray() / EndArray(n) 元素数 数组起止

    回调返回 false 立即终止解析(错误码 kParseErrorTermination)——这是 SAX 的剪枝开关。写一个统计器:

    struct CountHandler {
    unsigned objects = 0, arrays = 0,
    keys = 0, numbers = 0;
    std::string lastKey;
    bool StartObject() { ++objects; return true; }
    bool Key(const char* s, SizeType len, bool) {
    ++keys; lastKey.assign(s, len); return true;
    }
    bool EndObject(SizeType) { return true; }
    bool StartArray() { ++arrays; return true; }
    bool EndArray(SizeType) { return true; }
    bool Null() { return true; }
    bool Bool(bool) { return true; }
    bool Int(int) { ++numbers; return true; }
    bool Uint(unsigned) { ++numbers; return true; }
    bool Int64(int64_t) { ++numbers; return true; }
    bool Uint64(uint64_t) { ++numbers; return true; }
    bool Double(double) { ++numbers; return true; }
    bool String(const char*, SizeType, bool) { return true; }
    bool RawNumber(const char*, SizeType, bool) { return true; }
    };

    CountHandler h;
    Reader reader;
    StringStream ss(json);
    bool ok = reader.Parse(ss, h); // 事件直推,不建 DOM

    实测输出(解析 §4 的 json):

    sax ok=1 objects=2 arrays=1 keys=8 numbers=3 lastKey=port

    SAX 的价值:内存与输入规模无关,一遍流过;代价是只有一遍、没有随机访问。「一遍统计/过滤/校验」用它,「反复查询」用 DOM。

    10. 原位解析:把字符串留在原地

    原位(in-situ,拉丁语「在原处」):指算法直接在输入数据所在的内存上完成处理,不另辟缓冲——RapidJSON 官方中文文档译作「原位解析」,类比原位排序:无论输入多大,额外内存都是 O(1)。

    char buf[64];
    snprintf(buf, sizeof(buf),
    R"({"msg":"hello world"})");
    Document d;
    d.ParseInsitu(buf); // buf 必须可写!
    const char* p = d["msg"].GetString();
    // 实测:p 落在 buf 内部——零拷贝、零字符串分配

    ParseInsitu 把转义解码直接写回源缓冲(解码只会变短,原地写安全),DOM 里的字符串就是源文本本身。条件:缓冲可写、生命周期由你负责。省掉的不只是拷贝——是每个字符串一次的堆分配。原理与代价,下篇细拆。

    11. 常见坑(实测依据)

  • operator[] 与 GetXxx() 不做检查。d["ghost"] 缺键、d["x"].GetInt() 类型不符,默认走 assert(RAPIDJSON_ASSERT)——debug 崩给你看,release 是未定义行为。查询永远 FindMember + IsXxx。
  • 字符串默认引用,悬空最疼。AddMember("k", buf, a) 里 buf 是栈缓冲 → 值存的是指针,函数返回即悬空。要么 StringRef 明示引用意图,要么 SetString(buf, len, a) 显式拷贝(§7 表)。
  • 没有 value(key, default)。nlohmann 的缺省值读法在这边要手写:auto it = d.FindMember("k"); int v = it != d.MemberEnd() && it->value.IsInt() ? it->value.GetInt() : 30;
  • 跨 Document 转移要 CopyFrom。Value 的内存长在原 Document 的池里,直接把 docA["x"] 挂到 docB 上是灾难;用 docB["x"].CopyFrom(docA["x"], docB.GetAllocator())。
  • 生长会失效迭代器。AddMember/PushBack 可能触发底层 Realloc,之前拿的 MemberIterator/指针当场失效——迭代器和修改不要混线。
  • 深嵌套爆栈。默认递归下降,几千层嵌套能打穿调用栈;不受信输入加 kParseIterativeFlag(迭代式,栈深 O(1))。
  • 12. 延伸与联动

    • 与 nlohmann/json 的完整选型对比见第 2 篇 §5;一句话回顾:默认 nlohmann,热路径 RapidJSON,只读海量上 simdjson。
    • 官方教程含中文版(doc/tutorial.zh-cn.md),配本篇按类精读效果最好。
    • 这些 API 背后的性能机制——SIMD 跳空白、手写数字解析、原位零拷贝、24 字节 Value 与内存池——下一篇逐个拆开看源码。〔关联 第 4 篇〕

    参考:RapidJSON 仓库(v1.1.0,MIT)及其 doc/ 中文文档。文中代码与输出在 g++ 11.2(-std=c++17)实测;ParseFlags 表、拷贝语义表、Handler 签名均核对自源码注释。

    赞(0)
    未经允许不得转载:171主机测评 » 【C++三方组件】RapidJSON 上:API 分类精讲
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址