引言
在程序配置领域,TOML(Tom’s Obvious, Minimal Language)凭借其直观的语法和强类型特性,逐渐成为 JSON 和 YAML 的有力替代者。与 JSON 相比,TOML 支持注释、更易于手写;与 YAML 相比,TOML 语法更严格、歧义更少。
toml11 是一个现代 C++17 的 TOML 解析库,以header-only、类型安全、异常友好著称。utilsxx 将其作为外部组件集成到 lib/toml.hpp 中,方便项目直接使用。本文将基于 toml11 的完整功能,结合 utilsxx 中的示例,带你彻底掌握 TOML 在 C++ 中的读写技巧。
一、TOML 速览
1.1 为什么选择 TOML?
| 特性 | TOML | JSON | YAML |
|---|---|---|---|
| 注释支持 | ✅ | ❌ | ✅ |
| 强类型 | ✅ | ✅ | ❌(隐式转换多) |
| 手写友好 | ✅ | ⚠️ | ⚠️(缩进敏感) |
| 日期时间 | ✅ 原生 | ❌ | ✅ |
| 表(Table) | ✅ | ❌(用对象模拟) | ✅ |
| 歧义性 | 低 | 低 | 高 |
1.2 TOML 核心语法
# 这是注释
# 键值对(基本类型)
title = "TOML Example"
enabled = true
port = 8080
pi = 3.14159
# 表(类似 JSON 的对象)
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00
# 嵌套表
[servers.alpha]
ip = "10.0.0.1"
role = "frontend"
# 数组
ports = [8000, 8001, 8002]
# 表数组(数组的每个元素是一个表)
[[products]]
name = "Hammer"
sku = 738594937
[[products]]
name = "Nail"
sku = 284758393
二、快速上手
2.1 解析 TOML 文件
#include "toml.hpp"
#include <iostream>
int main() {
// 解析文件
const auto root = toml::parse("config.toml");
// 获取值
std::string title = toml::find<std::string>(root, "title");
std::cout << "Title: " << title << std::endl;
return 0;
}
2.2 两种访问方式
toml11 提供两种访问风格:
方式一:成员函数链式访问
std::string name = root.at("owner").at("name").as_string();
bool enabled = root.at("database").at("enabled").as_boolean();
int port0 = root.at("database").at("ports").at(0).as_integer();
方式二:toml::find 模板函数(推荐)
std::string name = toml::find<std::string>(root, "owner", "name");
bool enabled = toml::find<bool>(root, "database", "enabled");
std::vector<int> ports = toml::find<std::vector<int>>(root, "database", "ports");
toml::find 的优势:
- 类型安全:编译期确定返回类型
- 自动转换:可直接转换为
std::vector、std::map、std::pair等 STL 容器 - 异常信息友好:键不存在时抛出清晰的异常
三、基础类型访问
3.1 标量类型
# config.toml
name = "Alice"
age = 30
salary = 5000.50
active = true
const auto root = toml::parse("config.toml");
// 字符串
std::string name = toml::find<std::string>(root, "name");
// 整数
int age = toml::find<int>(root, "age");
// 浮点数
double salary = toml::find<double>(root, "salary");
// 布尔值
bool active = toml::find<bool>(root, "active");
3.2 成员函数对照表
当使用 at() 链式访问时,需要用对应的 as_*() 方法转换:
| TOML 类型 | 成员函数 | C++ 类型 |
|---|---|---|
| String | as_string() | std::string |
| Integer | as_integer() | std::int64_t |
| Float | as_floating() | double |
| Boolean | as_boolean() | bool |
| Datetime | as_offset_datetime() | toml::offset_datetime |
| Array | is_array() / at(i) | toml::value |
| Table | is_table() / at(key) | toml::value |
四、容器类型访问
4.1 数组(Array)
# array_example.toml
integers = [1, 2, 3]
colors = ["red", "yellow", "green"]
nested_arrays_of_ints = [[1, 2], [3, 4, 5]]
const auto root = toml::parse("data/array_example.toml");
// 方式1:逐个访问
int first = root.at("integers").at(0).as_integer(); // 1
std::string color = root.at("colors").at(1).as_string(); // "yellow"
// 方式2:整体转换为 vector
std::vector<int> integers = toml::find<std::vector<int>>(root, "integers");
std::vector<std::string> colors = toml::find<std::vector<std::string>>(root, "colors");
// 方式3:转换为固定大小数组
std::array<std::string, 3> color_arr = toml::find<std::array<std::string, 3>>(root, "colors");
// 嵌套数组
std::vector<std::vector<int>> nested =
toml::find<std::vector<std::vector<int>>>(root, "nested_arrays_of_ints");
4.2 表(Table)
# config.toml
[database]
enabled = true
ports = [8000, 8001, 8002]
[database.temp_targets]
cpu = 79.5
case = 72.0
const auto root = toml::parse("config.toml");
// 访问嵌套表
bool enabled = toml::find<bool>(root, "database", "enabled");
// 将表转换为 map
std::map<std::string, double> targets =
toml::find<std::map<std::string, double>>(root, "database", "temp_targets");
// targets["cpu"] == 79.5
// 将整个 database 表作为子树访问
const auto& db = toml::find(root, "database");
std::vector<int> ports = toml::find<std::vector<int>>(db, "ports");
4.3 混合类型数组
TOML 允许数组元素类型不同:
numbers = [0.1, 0.2, 0.5, 1, 2, 5]
// 转换为 tuple(前3个 double,后3个 int)
auto numbers = toml::find<std::tuple<double, double, double, int, int, int>>(root, "numbers");
double f1 = std::get<0>(numbers); // 0.1
int i1 = std::get<3>(numbers); // 1
4.4 表数组(Array of Tables)
这是 TOML 最强大的特性之一,适合表示列表数据:
[[products]]
name = "Hammer"
sku = 738594937
[[products]]
name = "Nail"
sku = 284758393
color = "gray"
const auto root = toml::parse("data/array_of_tables_example.toml");
// 方式1:逐个访问
std::string name0 = root.at("products").at(0).at("name").as_string(); // "Hammer"
int sku1 = root.at("products").at(1).at("sku").as_integer(); // 284758393
// 方式2:转换为 vector + map
std::vector<std::map<std::string, int>> products =
toml::find<std::vector<std::map<std::string, int>>>(root, "points");
// points[0]["x"] == 1
// 方式3:自定义结构体(推荐)
struct product_t {
product_t(const toml::value& v)
: name(toml::find_or<std::string>(v, "name", "")),
sku(toml::find_or<std::uint64_t>(v, "sku", 0)),
color(toml::find_or<std::string>(v, "color", ""))
{}
std::string name;
std::uint64_t sku;
std::string color;
};
std::vector<product_t> products = toml::find<std::vector<product_t>>(root, "products");
五、高级访问技巧
5.1 安全访问:find_or
当键可能不存在时,使用 find_or 提供默认值:
// 如果 "timeout" 不存在,返回 30
int timeout = toml::find_or(root, "timeout", 30);
// 如果 "user.email" 不存在,返回空字符串
std::string email = toml::find_or(root, "user", "email", std::string(""));
5.2 检查键是否存在
if (root.contains("optional_key")) {
auto val = toml::find<std::string>(root, "optional_key");
}
// 检查类型
if (root.at("maybe_string").is_string()) {
// ...
}
5.3 获取注释
toml11 会保留文件顶部的注释:
# This is a TOML document.
# This contains most of the examples in the spec.
[keys]
key = "value"
const auto root = toml::parse("data/key_example.toml");
assert(root.comments().size() == 2);
assert(root.comments().at(0) == " This is a TOML document.");
5.4 点键访问(Dotted Keys)
TOML 支持用点号表示嵌套:
fruits.apple.skin = "thin"
fruits.apple.color = "red"
std::string skin = toml::find<std::string>(root, "fruits", "apple", "skin"); // "thin"
std::string color = toml::find<std::string>(root, "fruits", "apple", "color"); // "red"
5.5 引号键与特殊键名
TOML 允许用引号定义包含特殊字符的键:
"127.0.0.1" = "value"
"character encoding" = "value"
"" = "blank"
std::string ip = toml::find<std::string>(root, "keys", "127.0.0.1");
std::string empty_key = toml::find<std::string>(root, "keys", "");
六、日期时间类型
TOML 原生支持多种日期时间格式:
[owner]
dob = 1979-05-27T07:32:00-08:00
const auto dob = toml::find<toml::offset_datetime>(root, "owner", "dob");
// 访问各个字段
assert(dob.date.year == 1979);
assert(dob.date.month == static_cast<int>(toml::month_t::May));
assert(dob.date.day == 27);
assert(dob.time.hour == 7);
assert(dob.time.minute == 32);
assert(dob.time.second == 0);
assert(dob.offset.hour == -8); // 时区偏移
日期时间类型对照:
| TOML 类型 | C++ 类型 | 示例 |
|---|---|---|
| Local Date | toml::local_date | 1979-05-27 |
| Local Time | toml::local_time | 07:32:00 |
| Local Datetime | toml::local_datetime | 1979-05-27T07:32:00 |
| Offset Datetime | toml::offset_datetime | 1979-05-27T07:32:00-08:00 |
七、自定义类型转换
toml11 的强大之处在于可以直接将 TOML 数据转换为自定义结构体:
7.1 从构造函数转换
struct contributor_t {
contributor_t(const toml::value& v) {
if (v.is_string()) {
name = v.as_string();
} else {
name = toml::find<std::string>(v, "name");
email = toml::find_or(v, "email", std::string(""));
url = toml::find_or(v, "url", std::string(""));
}
}
std::string name;
std::string email;
std::string url;
};
// 数组中混合了字符串和表,都能正确解析
std::vector<contributor_t> contributors =
toml::find<std::vector<contributor_t>>(root, "contributors");
7.2 复杂嵌套结构
struct fruit_t {
fruit_t(const toml::value& v)
: name(toml::find<std::string>(v, "name")),
physical(toml::find<std::map<std::string, std::string>>(v, "physical")),
varieties(toml::find<std::vector<std::map<std::string, std::string>>>(v, "varieties"))
{}
std::string name;
std::map<std::string, std::string> physical;
std::vector<std::map<std::string, std::string>> varieties;
};
std::vector<fruit_t> fruits = toml::find<std::vector<fruit_t>>(root, "fruits");
八、生成 TOML
除了解析,toml11 也支持生成 TOML 文件:
#include "toml.hpp"
int main() {
// 构建 TOML 值
toml::value root;
root["title"] = "My Application";
root["version"] = "1.0.0";
// 创建表
toml::value database;
database["host"] = "localhost";
database["port"] = 5432;
database["enabled"] = true;
root["database"] = database;
// 创建数组
root["features"] = toml::array{"auth", "logging", "metrics"};
// 序列化为字符串
std::string toml_str = toml::format(root);
std::cout << toml_str << std::endl;
// 保存到文件
std::ofstream file("output.toml");
file << toml_str;
return 0;
}
输出:
title = "My Application"
version = "1.0.0"
[database]
host = "localhost"
port = 5432
enabled = true
features = ["auth", "logging", "metrics"]
九、错误处理
toml11 使用异常报告错误,建议始终用 try-catch 包裹:
try {
const auto root = toml::parse("config.toml");
auto val = toml::find<int>(root, "nonexistent_key");
}
catch (const toml::syntax_error& e) {
// TOML 语法错误(如缺少引号、括号不匹配)
std::cerr << "Syntax error: " << e.what() << std::endl;
}
catch (const toml::type_error& e) {
// 类型不匹配(如尝试将字符串 as_integer())
std::cerr << "Type error: " << e.what() << std::endl;
}
catch (const toml::out_of_range& e) {
// 键不存在或数组越界
std::cerr << "Key not found: " << e.what() << std::endl;
}
十、完整示例:程序配置管理
以下是一个科学计算程序的完整 TOML 配置解析示例:
# simulation.toml
title = "Magnetic Inversion"
author = "Research Team"
[model]
file = "model.mesh"
tags = ["sedimentary", "igneous", "metamorphic"]
[computation]
range = [100.0, 1000.0, 200.0, 2000.0]
interval = [10, 10]
max_iterations = 5000
convergence_threshold = 1e-6
[output]
save_model = true
out_file = "result.vtk"
[[sensors]]
name = "Sensor-A"
position = [100.5, 200.3, 50.0]
active = true
[[sensors]]
name = "Sensor-B"
position = [150.2, 180.7, 45.0]
active = false
#include "toml.hpp"
#include <iostream>
#include <vector>
struct sensor_t {
sensor_t(const toml::value& v)
: name(toml::find<std::string>(v, "name")),
position(toml::find<std::vector<double>>(v, "position")),
active(toml::find_or(v, "active", true))
{}
std::string name;
std::vector<double> position;
bool active;
};
int main() try {
const auto root = toml::parse("simulation.toml");
// 基本信息
std::string title = toml::find<std::string>(root, "title");
std::cout << "Simulation: " << title << std::endl;
// 模型配置
std::string model_file = toml::find<std::string>(root, "model", "file");
std::vector<std::string> tags = toml::find<std::vector<std::string>>(root, "model", "tags");
// 计算参数
std::vector<double> range = toml::find<std::vector<double>>(root, "computation", "range");
int max_iter = toml::find<int>(root, "computation", "max_iterations");
double threshold = toml::find<double>(root, "computation", "convergence_threshold");
// 输出配置
bool save = toml::find<bool>(root, "output", "save_model");
std::string out = toml::find_or(root, "output", "out_file", std::string("default.out"));
// 传感器列表
std::vector<sensor_t> sensors = toml::find<std::vector<sensor_t>>(root, "sensors");
std::cout << "Sensors: " << sensors.size() << std::endl;
for (const auto& s : sensors) {
std::cout << " " << s.name << " at ("
<< s.position[0] << ", " << s.position[1] << ", " << s.position[2] << ")"
<< (s.active ? " [active]" : " [inactive]") << std::endl;
}
return 0;
}
catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
十一、API 速查表
解析
| 函数 | 说明 |
|---|---|
toml::parse(filename) | 解析 TOML 文件 |
toml::parse(str) | 从字符串解析 |
toml::parse(std::istream) | 从输入流解析 |
查询
| 函数 | 说明 |
|---|---|
toml::find<T>(value, keys...) | 按路径查找并转换为类型 T |
toml::find_or(value, key, default) | 安全查找,不存在返回默认值 |
value.at(key) | 成员函数访问 |
value.at(index) | 数组索引访问 |
value.contains(key) | 检查键是否存在 |
value.is_string() 等 | 类型检查 |
value.as_string() 等 | 类型转换 |
value.comments() | 获取注释 |
生成
| 函数 | 说明 |
|---|---|
toml::format(value) | 将值格式化为 TOML 字符串 |
toml::value | 通用值类型 |
toml::array{...} | 创建数组 |
结语
toml11 将 TOML 的简洁语法与 C++ 的类型安全完美结合。通过 toml::find,你可以用声明式的方式直接从 TOML 中提取类型正确的数据;通过自定义结构体的构造函数,复杂嵌套配置也能一键映射。对于需要手写配置文件的科学计算和工程应用,TOML + toml11 是一个值得长期投入的技术栈。
项目地址:utilsxx 上游项目:toruniina/toml11