跳到正文

目录

yaml-cpp 完整指南:C++ 生态最稳的 YAML 解析器

yaml-cpp 完整指南:C++ 生态最稳的 YAML 解析器

读完本文,你会知道怎么把 yaml-cpp 接进 CMake 项目、用 Node 读、用 Emitter 写,也知道它在什么场景该用、什么场景该换。文中所有代码都可以直接编译运行,建议跟着跑一遍。

前置条件:一个可用 CMake 3.15+ 的 C++ 工具链。示例基于 C++11。

关于 yaml-cpp

yaml-cpp 是纯 C++、无外部依赖的 YAML 解析与发射(emitter)库。它不追求极致性能,强项是可预测性:接口稳定、默认静态链接、从 C++11 一路兼容到新标准,0.5.x 起的现代 API 十年没有破坏性变化。把 YAML 当"配置 + 数据交换"的稳定锚点时,它是 C++ 生态里最省心的选择。

项目坐标

维度数据
仓库jbeder/yaml-cpp
Stars约 6k
主语言C++(当前要求 C++11+)
LicenseMIT
规范目标对齐 YAML 1.2
最新版本0.9.0
构建CMake(要求 3.15+)

什么场景选 yaml-cpp

C++ 处理 YAML 的基本路线:

选项优势劣势
yaml-cpp零依赖、API 稳定、维护活跃性能中等,无 schema 校验,不保留注释
rapidyaml(ryml)性能高一个量级、保留注释需要 C++17,API 更底层,学习曲线稍陡
libyaml(C 库)轻量、面向 YAML 1.1C 风格 API,自己管内存与错误,上手慢
Protobuf / FlatBuffers强 schema、高性能、跨语言不是给人读的格式,元数据改动要重生成

yaml-cpp 站在"中等性能 + 高可读性 + 强稳定性"这一档。配置文件、CI 参数、测试夹具这类场景绰绰有余。它解析速度不是最快的,如果你要"每请求解析一次 YAML",先看下方性能一节。

构建与链接

三种接法按场景选。

源码直接加进工程(嵌入式,最省事):

add_subdirectory(yaml-cpp)
target_link_libraries(my_app PRIVATE yaml-cpp::yaml-cpp)

系统级安装:

git clone https://github.com/jbeder/yaml-cpp.git
cd yaml-cpp && mkdir build && cd build
cmake -DYAML_CPP_BUILD_TESTS=OFF -DYAML_CPP_BUILD_TOOLS=OFF \
      -DYAML_CPP_BUILD_CONTRIB=OFF ..
cmake --build . -j
sudo cmake --install .

CMake 端消费:

find_package(yaml-cpp REQUIRED)
target_link_libraries(my_app PRIVATE yaml-cpp::yaml-cpp)

新版(0.8+)自带 CMake Package Config,yaml-cpp::yaml-cpp 是官方导入目标名。

用 FetchContent 固定版本(推荐,可复现):

include(FetchContent)
FetchContent_Declare(
  yaml-cpp
  GIT_REPOSITORY https://github.com/jbeder/yaml-cpp.git
  GIT_TAG 0.9.0
)
FetchContent_MakeAvailable(yaml-cpp)

编译开关说明YAML_CPP_BUILD_TOOLSYAML_CPP_BUILD_CONTRIB 默认是 ON,会额外构建解析命令行工具和 contrib 代码;YAML_CPP_BUILD_TESTS 默认由 BUILD_TESTING 决定,主项目默认开启。这些不会编进 yaml-cpp 库本身,但会拖长构建。生产构建显式关掉它们能省不少时间。

实现细节:yaml-cpp 默认构建静态库。要动态库加 -DYAML_BUILD_SHARED_LIBS=ON;Windows 下静态链接静态 CRT 时用 -DYAML_MSVC_SHARED_RT=ON 切换成动态 CRT。

Node API:读 YAML

yaml-cpp 用 YAML::Node 表达一切节点——map、序列(sequence)、标量(scalar)和 null。

读取基础类型

#include <yaml-cpp/yaml.h>
#include <iostream>

int main() {
    // 文件不存在抛 YAML::BadFile
    YAML::Node config = YAML::LoadFile("config.yaml");

    std::string name = config["service"]["name"].as<std::string>();
    int port = config["service"]["port"].as<int>();
    bool tls = config["service"]["tls"].as<bool>();

    std::cout << name << " on port " << port << " (tls=" << tls << ")\n";
}

YAML::LoadFile 走文件系统;只加载字符串内容用 YAML::Load(str)。一个文档里多个根节点用 YAML::LoadAll,返回节点迭代器。

安全取值:缺键与类型错

as<T>() 碰到类型不符会抛 YAML::TypedBadConversion<T>,缺键访问返回 undefined 节点。生产代码按下面几种方式防御:

// 缺键:先 IsDefined 再取值
std::string name = config["service"]["name"].IsDefined()
    ? config["service"]["name"].as<std::string>()
    : "default-name";

// 类型错 / 键缺失:只捕获 YAML 派生异常,别吞 RuntimeError
try {
    port = config["service"]["port"].as<int>();
} catch (const YAML::TypedBadConversion<int>& e) {
    LOG(WARNING) << "port 缺失或类型不对,使用默认值 8080";
    port = 8080;
}

监听真实异常链即可:YAML::BadFile(打不开文件)、YAML::ParserException(YAML 语法错)、YAML::TypedBadConversion<T>(类型转换失败)都继承自 YAML::Exception。需要全局兜底时 catch YAML::Exception,不用接住不相关的 std::runtime_error

一个可复用的按路径取值工具:

template <typename T>
T get_or(const YAML::Node& n, const std::string& path, T default_value) {
    try {
        YAML::Node cur = n;
        for (const auto& part : split(path, ".")) {
            if (!cur[part].IsDefined()) return default_value;
            cur = cur[part];
        }
        return cur.as<T>();
    } catch (const YAML::Exception&) {
        return default_value;
    }
}

splitstd::getline'.' 分隔的实现,这里不展开。

判断节点类型

远端传入的结构未必是你预期的。读之前先看形状:

YAML::Node cfg = YAML::Load(remote_str);
if (cfg.IsMap())        { /* 键值对 */ }
else if (cfg.IsSequence()) { /* 数组 */ }
else if (cfg.IsScalar())   { /* 标量 */ }
else if (cfg.IsNull())     { /* ~ 或 null */ }

遍历序列

replicas:
  - host: 10.0.0.1
    port: 8080
  - host: 10.0.0.2
    port: 8080
for (const auto& node : config["replicas"]) {
    std::string host = node["host"].as<std::string>();
    int port = node["port"].as<int>();
    // ...
}

遍历 map

YAML::Node env = config["environments"];
for (auto it = env.begin(); it != env.end(); ++it) {
    std::string name = it->first.as<std::string>();
    YAML::Node cfg = it->second;
    // ...
}

Emitter API:写 YAML

YAML::Emitter 是流式生成器。手动拼 map:

#include <yaml-cpp/yaml.h>
#include <iostream>

int main() {
    YAML::Emitter out;
    out << YAML::BeginMap;
    out << YAML::Key << "service" << YAML::Value;
    out << YAML::BeginMap;
    out << YAML::Key << "name" << YAML::Value << "api-gateway";
    out << YAML::Key << "port" << YAML::Value << 8080;
    out << YAML::Key << "tls" << YAML::Value << true;
    out << YAML::EndMap;
    out << YAML::EndMap;

    std::cout << out.c_str() << "\n";
    return 0;
}

输出:

service:
  name: api-gateway
  port: 8080
  tls: true

序列里嵌 map:

YAML::Emitter out;
out << YAML::BeginSeq;
for (const auto& item : items) {
    out << YAML::BeginMap;
    out << YAML::Key << "id" << YAML::Value << item.id;
    out << YAML::Key << "name" << YAML::Value << item.name;
    out << YAML::EndMap;
}
out << YAML::EndSeq;

更常见的做法是构造 Node 再发射,逻辑更直白:

YAML::Node root(YAML::NodeType::Map);
root["name"] = "api-gateway";
root["port"] = 8080;

YAML::Emitter out;
out << root;

Node::operator<< 支持发射已构造的节点,适合"读回来、改几个字段、再写出去"的场景。

注释保留:yaml-cpp 的边界

YAML 规范里注释不是数据结构,yaml-cpp 解析时直接丢弃:读进来再写出去,原注释全部消失。如果你的配置文件需要人工编辑且保留注释——CI/CD 配置、Helm 模板、Ansible Playbook 这类——这是硬伤,应该选 rapidyaml(ryml)。反过来,纯机器生成、机器消费的配置,注释丢了无所谓。

字符串与类型的坑

特殊字符要引号

: (冒号空格)和 # 在 YAML 里有语法含义:

url: "https://example.com:8080"   # 不引会出现歧义
color: "red"

url: https://example.com:8080 无引号,: 后跟空格会被解析成内联 map,得到错误结构。URL、带 : /# 的字符串务必加引号。

多行字符串

desc: |
  这是一段
  多行字符串(保留换行)
flat: >
  这是一段
  折叠字符串(换行变空格)

| 保留换行,> 把换行折成空格。as<std::string>() 拿到的都是处理后的结果。

数字 vs 字符串

version: 1.10   # as<double>() 得到 1.1
id: "001"       # 字符串 "001",不会被当成数字 1

YAML 标量按内容推断类型:前导零、尾零的数值会被浮点规则改写(1.101.1)。要保真,用引号写成字符串。版本号、ID、手机号这类建议一律引号包住。

性能怎么评估

yaml-cpp 没有官方 benchmark,网上能查到的对比也随文档复杂度和编译器波动。给你几条可落地的判断,而不是拍一个数字:

  • 定性:它是为"读配置文件"写的,不是为"热路径每请求解析"写的。
  • 实测:别信别人的数字,用你自己的配置样文件在目标机器跑一遍,决定要不要换。
  • 方向:启动时一次性解析并缓存,别在循环里反复 Load;传输协议别用 YAML,上 Protobuf,避开可读性问题。

如果你确认 yaml-cpp 是瓶颈,rapidyaml(ryml)是 C++ 里常见的升级路径,性能通常快一个数量级。

YAML 什么时候不该用

适合 YAML

  • 配置文件(开发友好,可读性优先)
  • CI/CD 流水线(GitHub Actions、GitLab CI)
  • 测试夹具、数据驱动的单测
  • 部署清单(Helm、Ansible)

不适合 YAML

  • 跨网络传输的消息(用 Protobuf、Cap’n Proto)
  • 高频热路径解析(用 FlatBuffers、MessagePack)
  • 二进制数据
  • 需要 schema 严格校验的场景

决策逻辑:给"人看的配置"用 YAML,给"机器跑的消息"用二进制格式。

常见问题

Q:读文件抛 YAML::BadFile 文件路径不存在或没权限。先确认路径,再确认不是只写了文件名而未写目录。

Q:as<int> 一直抛 TypedBadConversion YAML 里 port: 8080 默认是字符串 "8080"as<int> 能转;但 port: 0x1F90 这种带前缀的会失败。看源文件实际标量,缺引号或格式不符就转换失败。

Q:怎样让 Emitter 输出带类型注解的文档? Emitter 默认按值的 C++ 类型推断。要写复杂类型注释,用 out << YAML::Comment("...") 手动补(但读回仍丢注释)。

Q:0.5.0 之前的旧 API 还能用吗? 0.5.x 起是双向分离的 Node/Emitter API,旧 API 只存在于 0.3.x 分支,已停止维护,新代码别用。

维护信号

  • 版本:跟进 0.9.0 发行说明,接口变动都写在 release notes。
  • 参考:Wiki 的 Tutorial 和 How to Emit YAML 是官方最权威的用法来源(见参考资源)。
  • 迁移:load() 旧接口 → Load/LoadFileNode::set → 赋值或 as<T>() 落地。

术语速查

术语含义
Node节点,map/sequence/scalar/null 的统一抽象
Emitter流式 YAML 生成器
Load把字符串/文件解析成 Node
ManipulatorYAML::Key/YAML::Value/YAML::BeginSeq 等流控制标记

参考资源

参与讨论

使用 GitHub 登录。欢迎补充事实、异议与实践。