引言

在程序配置领域,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?

特性TOMLJSONYAML
注释支持
强类型❌(隐式转换多)
手写友好⚠️⚠️(缩进敏感)
日期时间✅ 原生
表(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::vectorstd::mapstd::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++ 类型
Stringas_string()std::string
Integeras_integer()std::int64_t
Floatas_floating()double
Booleanas_boolean()bool
Datetimeas_offset_datetime()toml::offset_datetime
Arrayis_array() / at(i)toml::value
Tableis_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 Datetoml::local_date1979-05-27
Local Timetoml::local_time07:32:00
Local Datetimetoml::local_datetime1979-05-27T07:32:00
Offset Datetimetoml::offset_datetime1979-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