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=0、Scalar=1、Sequence=2、Map=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)。
总结
- 核心基石 :
YAML::Node是所有操作的核心,需掌握其类型判断、访问、遍历、修改,区分[](自动创建)和at()(安全访问)的差异; - 版本与编译 :优先使用 0.6.x+ 版本,CMake 集成时正确链接
yaml-cpp::yaml-cpp,注意 C++11 及以上标准要求; - 读写能力 :覆盖单/多文档读写,基础写入用
<<,自定义格式用YAML::Emitter; - 高级特性 :自定义类型序列化需特化
YAML::convert<T>,异常处理分异常式(捕获)和非异常式(判断); - 最佳实践:大文件用流解析,避免频繁加载,处理中文需保证 UTF-8 编码,版本差异是核心踩坑点。