C++---yaml-cpp YAML标准解析/生成库

yaml-cpp 是纯 C++ 实现的 YAML 1.2 标准解析/生成库,无外部依赖,是 C++ 生态中处理 YAML 配置、数据序列化的工业级解决方案。其核心优势在于类型安全API 灵活性能优异,但易因版本差异、API 细节踩坑。

一、核心基础:版本与编译配置

yaml-cpp 的版本差异是新手最大的陷阱:0.5.x 为旧版(API 不统一),0.6.x+ 为新版(重构后更规范,本文以 0.7.0 为例),两者核心 API 不兼容,需先明确版本选择。

1. 编译配置(源码/CMake 集成)

(1)源码编译关键选项
bash 复制代码
git clone https://github.com/jbeder/yaml-cpp.git
cd yaml-cpp && mkdir build && cd build
# 核心编译选项(按需配置)
cmake \
  -DCMAKE_BUILD_TYPE=Release \          # 生产环境用Release(优化性能)
  -DYAML_CPP_BUILD_STATIC=ON \          # 编译静态库(默认动态库)
  -DYAML_CPP_BUILD_TESTS=OFF \          # 关闭测试(加速编译)
  -DYAML_CPP_BUILD_TOOLS=OFF \          # 关闭工具编译
  -DCMAKE_INSTALL_PREFIX=/usr/local \   # 安装路径
  ..
make -j$(nproc) && sudo make install
(2)CMake 工程集成(实际开发首选)

CMakeLists.txt 中正确链接 yaml-cpp 是工程化的关键:

cmake 复制代码
cmake_minimum_required(VERSION 3.10)
project(YamlCppDemo)

# 设置C++标准(yaml-cpp需C++11及以上)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 查找yaml-cpp库
find_package(yaml-cpp REQUIRED)

# 添加可执行文件
add_executable(demo main.cpp)

# 链接库(关键:新版用yaml-cpp::yaml-cpp,旧版直接写yaml-cpp)
target_link_libraries(demo PRIVATE yaml-cpp::yaml-cpp)

2. 版本核心差异(0.5.x vs 0.6.x+)

特性 0.5.x(旧版) 0.6.x+(新版)
迭代器类型 YAML::Iterator YAML::Node::iterator
多文档加载 LoadAll 返回 std::vector<Node> 同左,但API更稳定
节点判断 node.Type() 保留,新增IsMap()/IsSequence()
异常体系 简陋 细分BadFile/TypedBadConversion等

二、核心数据结构:YAML::Node

YAML::Node 是 yaml-cpp 的核心抽象,表示 YAML 中的任意元素(标量、列表、映射、空值),所有操作均围绕该类展开,需掌握其完整用法。

yaml-cpp 的 YAML::Node 是动态类型容器,不需要像声明 int/std::vector 那样显式指定 "这是标量节点 / 序列节点",而是通过对节点的操作行为(赋值、push_back、[key] 赋值等),让库自动推导并切换节点类型

1. 节点类型与判断

YAML 节点分为 4 类核心类型,可通过 Type() 或便捷方法判断:

cpp 复制代码
#include <yaml-cpp/yaml.h>
#include <iostream>

int main() {
    // 1. 标量节点(字符串/数字/布尔)
    YAML::Node scalar_node = "hello yaml-cpp";
    std::cout << "标量节点类型:" << scalar_node.Type() << std::endl; // 输出1(Scalar)
    std::cout << "是否为标量:" << scalar_node.IsScalar() << std::endl; // true

    // 2. 序列节点(列表/数组)
    YAML::Node seq_node;//type2
    seq_node.push_back(10);
    seq_node.push_back(20);
    std::cout << "是否为序列:" << seq_node.IsSequence() << std::endl; // true

    // 3. 映射节点(键值对/字典)
    YAML::Node map_node;//type3
    map_node["name"] = "yaml-cpp";
    map_node["version"] = "0.7.0";
    std::cout << "是否为映射:" << map_node.IsMap() << std::endl; // true

    // 4. 空节点
    YAML::Node null_node; //type0
    std::cout << "是否为空:" << null_node.IsNull() << std::endl; // true
    return 0;
}

注:Type() 返回枚举值:Null=0Scalar=1Sequence=2Map=3

2. 节点的访问与遍历

(1)映射节点访问(两种方式)
  • node[key]:无该键则自动创建空节点(易踩坑);
  • node.at(key):无该键则抛出异常(类型安全)。
cpp 复制代码
YAML::Node config = YAML::LoadFile("config.yaml");
// 安全访问:不存在则抛YAML::KeyNotFound异常
std::string db_host = config["database"].at("host").as<std::string>();
// 非安全访问:不存在则创建空节点,后续as<T>()会抛TypedBadConversion
std::string db_pass = config["database"]["password"].as<std::string>();
(2)序列节点访问
cpp 复制代码
YAML::Node seq_node = YAML::Load("[1,2,3,4]");
// 下标访问
int first = seq_node[0].as<int>();
// 迭代器遍历
for (YAML::const_iterator it = seq_node.begin(); it != seq_node.end(); ++it) {
    std::cout << it->as<int>() << " ";
}
// C++11范围for遍历(推荐)
for (const auto& elem : seq_node) {
    std::cout << elem.as<int>() << " ";
}
(3)映射节点遍历
cpp 复制代码
YAML::Node map_node = YAML::Load("{name: yaml-cpp, version: 0.7.0}");
// 迭代器遍历(键值对)
for (YAML::const_iterator it = map_node.begin(); it != map_node.end(); ++it) {
    std::string key = it->first.as<std::string>();
    std::string value = it->second.as<std::string>();
    std::cout << key << ": " << value << std::endl;
}
// C++11范围for遍历
for (const auto& pair : map_node) {
    std::cout << pair.first.as<std::string>() << ": " << pair.second.as<std::string>() << std::endl;
}

3. 节点的修改与删除

cpp 复制代码
YAML::Node map_node;
// 添加/修改键值对
map_node["author"] = "jbeder";
map_node["stars"] = 4.8;

// 删除节点(新版支持erase,旧版需手动置空)
if (map_node["stars"]) { // 先判断节点是否存在
    map_node.erase("stars");
}

// 清空节点
map_node["author"] = YAML::Null; // 置为空
map_node.clear(); // 清空所有子节点

三、完整读写流程:解析与生成

1. 读取 YAML

(1)从文件读取(单文档)
cpp 复制代码
try {
    YAML::Node config = YAML::LoadFile("config.yaml");
} catch (const YAML::BadFile& e) {
    std::cerr << "文件不存在/无法读取:" << e.what() << std::endl;
}
(2)从字符串读取
cpp 复制代码
std::string yaml_str = "name: test\nage: 20";
YAML::Node node = YAML::Load(yaml_str);
(3)读取多文档 YAML

YAML 支持单个文件包含多个文档(用---分隔),需用 LoadAll

yaml 复制代码
# multi_doc.yaml
---
name: doc1
value: 10
---
name: doc2
value: 20
cpp 复制代码
std::vector<YAML::Node> docs = YAML::LoadAllFromFile("multi_doc.yaml");
for (size_t i = 0; i < docs.size(); ++i) {
    std::cout << "文档" << i+1 << ":" << docs[i]["name"].as<std::string>() << std::endl;
}
// 输出:文档1:doc1;文档2:doc2

2. 生成 YAML(基础/高级格式控制)

(1)基础写入(直接序列化)
cpp 复制代码
YAML::Node node;
node["server"]["port"] = 8080;
node["server"]["ip"] = "0.0.0.0";

// 写入文件
std::ofstream fout("output.yaml");
fout << node;
fout.close();

// 输出到控制台
std::cout << node << std::endl;
(2)高级格式控制(YAML::Emitter)

直接用<<生成的 YAML 格式固定,需自定义缩进、引号、换行时,用 YAML::Emitter

cpp 复制代码
YAML::Emitter emitter;
// 设置格式:2空格缩进、启用换行、标量用双引号
emitter.SetIndent(2);
emitter.SetNewline("\n");
emitter.SetQuotedStrings(true);

// 构建映射节点
emitter << YAML::BeginMap;
emitter << YAML::Key << "app";
emitter << YAML::Value << YAML::BeginMap;
emitter << YAML::Key << "name" << YAML::Value << "MyApp";
emitter << YAML::Key << "version" << YAML::Value << "1.0.0";
emitter << YAML::EndMap;
emitter << YAML::EndMap;

// 输出结果
std::cout << emitter.c_str() << std::endl;

生成的 YAML:

yaml 复制代码
app:
  "name": "MyApp"
  "version": "1.0.0"
(3)写入多文档 YAML
cpp 复制代码
std::ofstream fout("multi_output.yaml");
// 第一个文档
YAML::Node doc1;
doc1["name"] = "doc1";
fout << doc1 << "---\n";

// 第二个文档
YAML::Node doc2;
doc2["name"] = "doc2";
fout << doc2;
fout.close();

YAML 的 "多文档" 不是指 "多个文件",而是指 "单个文件内包含多个独立的 YAML 文档

单个文件中可以包含多个独立的 YAML 文档,文档之间用 ---(三个连字符)分隔;如果是文档结束,可选 ... 收尾(非必须)

四、类型转换:基础/自定义类型

1. 基础类型转换

as<T>() 支持所有基础类型和标准容器:

cpp 复制代码
YAML::Node node = YAML::Load("str: hello\nnum: 10\nbool: true\nlist: [1,2,3]");

// 基础类型
std::string s = node["str"].as<std::string>();
int n = node["num"].as<int>();
bool b = node["bool"].as<bool>();

// 标准容器
std::vector<int> vec = node["list"].as<std::vector<int>>();
std::map<std::string, int> map_node = YAML::Load("{a:1, b:2}").as<std::map<std::string, int>>();

2. 自定义类型序列化(核心高级特性)

需特化 YAML::convert<T> 模板,实现 encode/decode 方法:

cpp 复制代码
// 自定义类型
struct Person {
    std::string name;
    int age;
    double score;
};

// 特化转换模板
namespace YAML {
template<>
struct convert<Person> {
    // 解码:Node → Person
    static bool decode(const Node& node, Person& p) {
        // 检查必要字段是否存在
        if (!node.IsMap() || !node["name"] || !node["age"]) {
            return false;
        }
        p.name = node["name"].as<std::string>();
        p.age = node["age"].as<int>();
        p.score = node["score"].as<double>(0.0); // 可选字段,默认0.0
        return true;
    }

    // 编码:Person → Node
    static Node encode(const Person& p) {
        Node node;
        node["name"] = p.name;
        node["age"] = p.age;
        node["score"] = p.score;
        return node;
    }
};
} // namespace YAML

// 使用示例
int main() {
    // 序列化
    Person p{"Alice", 20, 95.5};
    YAML::Node node = p;
    std::cout << node << std::endl;

    // 反序列化
    YAML::Node p_node = YAML::Load("name: Bob\nage: 22\nscore: 88.0");
    Person p2 = p_node.as<Person>();
    std::cout << p2.name << " " << p2.age << std::endl; // 输出Bob 22
    return 0;
}

五、异常处理与非异常检查

yaml-cpp 提供异常式非异常式两种错误处理方式,需根据场景选择:

1. 异常体系(核心异常类型)

异常类 触发场景
YAML::BadFile 文件不存在/无法读取
YAML::KeyNotFound at(key)访问不存在的键
YAML::TypedBadConversion as<T>()类型转换失败
YAML::InvalidNode 访问已失效/空节点
cpp 复制代码
try {
    YAML::Node node = YAML::LoadFile("config.yaml");
    int port = node["server"].at("port").as<int>();
} catch (const YAML::BadFile& e) {
    std::cerr << "文件错误:" << e.what() << std::endl;
} catch (const YAML::KeyNotFound& e) {
    std::cerr << "键不存在:" << e.what() << std::endl;
} catch (const YAML::TypedBadConversion& e) {
    std::cerr << "类型转换失败:" << e.what() << std::endl;
}

2. 非异常式检查(避免频繁捕获异常)

cpp 复制代码
YAML::Node node = YAML::LoadFile("config.yaml");
// 检查节点是否存在
if (node["server"] && node["server"]["port"]) {
    // 安全转换(先判断类型)
    if (node["server"]["port"].IsScalar()) {
        int port = node["server"]["port"].as<int>();
    }
}

六、高级特性与最佳实践

1. 锚点与别名处理(YAML &/*)

YAML 支持锚点(&)和别名(*)复用数据,yaml-cpp 可正确解析:

yaml 复制代码
# anchor.yaml
base: &base_config
  port: 8080
  ip: 0.0.0.0

dev: *base_config
prod:
  <<: *base_config
  port: 80
cpp 复制代码
YAML::Node node = YAML::LoadFile("anchor.yaml");
std::cout << "dev.port: " << node["dev"]["port"].as<int>() << std::endl; // 8080
std::cout << "prod.port: " << node["prod"]["port"].as<int>() << std::endl; // 80

2. 大文件处理(流解析)

加载超大 YAML 文件时,LoadFile 会一次性加载到内存,需用流解析:

cpp 复制代码
std::ifstream fin("large.yaml");
YAML::Parser parser(fin); // 旧版0.5.x接口,新版可用YAML::InputStream
YAML::Node node;
while (parser.GetNextDocument(node)) {
    // 逐文档处理
    std::cout << node["name"].as<std::string>() << std::endl;
}

3. 性能优化

  • 避免频繁 LoadFile:加载一次后缓存 YAML::Node
  • 减少拷贝:用 const YAML::Node& 引用而非值传递;
  • 关闭调试信息:编译时加 -DNDEBUG 禁用断言。

4. 常见踩坑点

  • 中文乱码:确保 YAML 文件编码为 UTF-8,读取时用 std::wstring 或保持 UTF-8;
  • 0.5.x 迭代器失效:遍历中修改节点会导致迭代器失效,需先收集键再修改;
  • 空节点 as<T>() 崩溃:务必先判断 node.IsDefined() 再转换;
  • 动态库链接错误:编译时确保程序和 yaml-cpp 用相同的 C++ 标准(如均为 C++11)。

总结

  1. 核心基石YAML::Node 是所有操作的核心,需掌握其类型判断、访问、遍历、修改,区分 [](自动创建)和 at()(安全访问)的差异;
  2. 版本与编译 :优先使用 0.6.x+ 版本,CMake 集成时正确链接 yaml-cpp::yaml-cpp,注意 C++11 及以上标准要求;
  3. 读写能力 :覆盖单/多文档读写,基础写入用 <<,自定义格式用 YAML::Emitter
  4. 高级特性 :自定义类型序列化需特化 YAML::convert<T>,异常处理分异常式(捕获)和非异常式(判断);
  5. 最佳实践:大文件用流解析,避免频繁加载,处理中文需保证 UTF-8 编码,版本差异是核心踩坑点。
相关推荐
96772 小时前
多线程编程:整个互斥的流程以及scoped_lock的用法,以及作用,以及 硬件上的原子操作和逻辑上的原子操作
开发语言·c++·算法
liuyao_xianhui2 小时前
优选算法_topk问题_快速排序算法_堆_C++
java·开发语言·数据结构·c++·算法·链表·排序算法
yunn_2 小时前
Qt智能指针
c++·qt
liuyao_xianhui2 小时前
优选算法_堆_最后一块石头的重量_C++
java·开发语言·c++·算法·链表
上天_去_做颗惺星 EVE_BLUE2 小时前
Linux Core Dump 测试操作手册
linux·c++·测试工具
羊小猪~~2 小时前
算法/力扣--栈与队列经典题目
开发语言·c++·后端·考研·算法·leetcode·职场和发展
云泽8082 小时前
深入红黑树:SGI-STL 中 map 与 set 的关联容器架构剖析
开发语言·c++·stl底层架构
福楠2 小时前
constexpr 全家桶
c语言·开发语言·c++
REDcker2 小时前
C++ vcpkg:安装、使用、原理与选型
开发语言·c++·windows·操作系统·msvc·vcpkg