引言
几乎每个非平凡的程序都需要配置参数:输入文件路径、算法阈值、输出格式、模型参数……硬编码这些值会让程序失去灵活性,而手写解析逻辑又繁琐且容易出错。
getoption 是 utilsxx 库中的轻量级配置解析组件。它采用声明式设计理念:你先声明程序需要哪些参数、哪些是必填的、哪些参数属于互斥组,然后从文件或字符串向量中读取配置,getoption 会自动完成解析、校验和类型转换。本文将带你从基础用法到高阶技巧,彻底掌握这个工具。
一、设计理念:声明式配置管理
传统的配置解析往往是"命令式"的:读一行、判断键名、手动转换类型、检查是否缺失。getoption 将其转变为"声明式":
// 1. 声明:我的程序需要这些参数
getoption gopt;
gopt.add_options({"range", "interval", "output"}, {true, true, false});
// 2. 读取:从配置文件加载
gopt.read_options("config.txt");
// 3. 校验:自动检查必填项和互斥组
gopt.check_mandatory();
// 4. 使用:直接获取已转换类型的值
auto range = gopt.get_value<std::string>("range");
auto weight = gopt.get_values<double>("weight", '|', '/');
这种设计的优势:
- 集中管理:所有参数定义在一处,一目了然
- 自动校验:必填项、互斥组自动检查,无需手写
if判断 - 类型安全:模板函数自动完成字符串到目标类型的转换
- 别名支持:一个参数可以有多个名称(如
model-file/ModelFile/model_file)
二、快速上手
2.1 配置文件格式
getoption 使用简洁的 key-value 格式:
# 以 # 开头的行是注释,会被自动跳过
range = 100/1000/200/2000/full_range
interval = 10/10
weight = 120.12/232/114.2
model-file = test.txt
# 空行也会被跳过,不影响解析
# 同一个 key 可以出现多次,值会自动合并
model-tag = model1
model-tag = model2
model-tag = model3
out-model = example.msh
格式规则:
- 注释行:以
#开头(可自定义) - 空行:自动跳过
- 参数行:
key <分隔符> value,默认分隔符为= - 多值合并:同一 key 多次出现,值用连接符拼接(默认逗号
,)
2.2 基础代码示例
#include "get_option.hpp"
#include <iostream>
int main() try {
// 步骤1:声明参数
utilsxx::getoption gopt;
gopt.add_options(
{"range", "interval", "weight", "model-file", "output"}, // 参数名
{true, true, false, true, false} // 是否必填
);
// 步骤2:读取配置文件
gopt.read_options("config.txt");
// 步骤3:校验必填项
gopt.check_mandatory();
// 步骤4:获取参数值
std::string model = gopt.get_value<std::string>("model-file");
std::vector<double> w = gopt.get_values<double>("weight", '|', '/');
std::cout << "Model: " << model << "\n";
std::cout << "Weight count: " << w.size() << "\n";
return 0;
}
catch(utilsxx::error_handler &e) {
e.show();
return 1;
}
三、参数声明详解
3.1 逐个添加参数
gopt.add_option("range", true); // 必填参数
gopt.add_option("output", false); // 可选参数
gopt.add_option("verbose", false, 1); // 可选参数,属于组1
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
opt_name | string | 参数名称 |
manda | bool | 是否必填(true = 必填) |
gp | int | 所属组号(-1 = 不属于任何组) |
3.2 批量添加参数
gopt.add_options(
{"range", "interval", "weight", "model-file", "model-tag", "out-model", "save-model"},
{true, true, true, true, true, true, false}
);
两个 initializer_list 必须长度一致,否则会抛出异常。
3.3 设置参数分组(互斥/至少选一)
分组用于实现"至少选一个"的校验逻辑:
// out-model 和 save-model 属于组1,两者至少填一个
gopt.set_group(1, {"out-model", "save-model"});
校验时:
gopt.check_group(1); // 检查组1:至少一个参数被设置
gopt.check_groups(); // 检查所有组
gopt.check_mandatory(); // 检查所有必填项(组内参数不检查)
注意:属于分组的参数在
check_mandatory()中不会被检查,因为组校验已经保证了"至少一个"的存在性。
四、读取配置
4.1 从文件读取
gopt.read_options(
"data/option_sample", // 文件名(自动追加 .txt)
"=", // key-value 分隔符(默认 =)
",", // 多值连接符(默认 ,)
'#' // 注释符(默认 #)
);
4.2 从字符串向量读取
适合从命令行参数或其他内存数据源读取:
std::vector<std::string> opts = {
"# this is an annotation",
"range = 100/1000/200/2000/full_range",
"interval = 10/10",
"weight = 120.12/232/114.2",
"model-file = test.txt",
"", // 空行会被跳过
"model-tag = model1",
"model-tag = model2",
"model-tag = model3",
"out-model = example.msh"
};
gopt.read_options(opts); // 参数与文件版相同
4.3 多值合并行为
当同一个 key 在配置文件中多次出现时,值会自动合并:
model-tag = model1
model-tag = model2
model-tag = model3
读取后 model-tag 的值为:model1,model2,model3(默认用逗号连接)
你可以自定义连接符:
// 用分号连接多值
gopt.read_options("config.txt", "=", ";");
五、获取参数值
5.1 获取单个值
// 获取字符串值
std::string model = gopt.get_value<std::string>("model-file");
// 获取 double 值(自动类型转换)
double val = gopt.get_value<double>("threshold");
// 获取 int 值
int count = gopt.get_value<int>("max-iterations");
// 获取 bool 值
bool verbose = gopt.get_value<bool>("verbose");
5.2 获取多值(向量)
配置文件中的值本身可以包含分隔符,用 get_values() 解析为向量:
weight = 120.12/232/114.2
// 用 '/' 作为值内分隔符,解析为 double 向量
std::vector<double> weight = gopt.get_values<double>("weight", '|', '/');
// 结果: {120.12, 232.0, 114.2}
参数说明:
| 参数 | 说明 |
|---|---|
key | 参数名(可含别名,如 "weight|Weight") |
delimiter | key 别名之间的分隔符(默认 |) |
sep | 值内部的分隔符(默认空格) |
5.3 参数别名
一个参数可以有多个名称,用 | 分隔:
// 以下调用都会匹配同一个参数
auto w1 = gopt.get_value<std::string>("model-file");
auto w2 = gopt.get_value<std::string>("Model-File");
auto w3 = gopt.get_value<std::string>("model_file");
// 更简洁的写法:一次声明多个别名
auto w = gopt.get_value<std::string>("model-file|Model-File|model_file");
这在处理不同命名风格的配置文件时非常有用(如用户可能写 model-file、ModelFile 或 model_file)。
5.4 检查参数是否存在
if (gopt.has_value("output")) {
std::string out = gopt.get_value<std::string>("output");
}
has_value() 同样支持别名:
if (gopt.has_value("output|Output|out")) {
// ...
}
六、校验机制
6.1 必填项校验
gopt.add_options({"input", "output", "threshold"}, {true, true, false});
gopt.read_options("config.txt");
gopt.check_mandatory(); // 如果 input 或 output 未设置,抛出异常
错误输出示例:
getoption: mandatory option not set: input
getoption: mandatory option not set: output
6.2 分组校验(至少选一)
gopt.add_options({"out-model", "save-model"}, {false, false});
gopt.set_group(1, {"out-model", "save-model"});
gopt.read_options("config.txt");
gopt.check_group(1); // 两者至少填一个,否则抛出异常
错误输出示例:
getoption: need at least one of grouped options:
out-model
save-model
6.3 完整校验流程
gopt.read_options("config.txt");
gopt.check_mandatory(); // 先检查必填项
gopt.check_groups(); // 再检查组约束
最佳实践:总是在
read_options()之后立即调用校验函数,尽早发现配置错误。
七、高级技巧
7.1 结合 str_utils 解析复杂值
getoption 与 str_utils.hpp 中的工具函数配合,可以解析更复杂的值格式:
// 配置文件:range = 100/1000/200/2000/full_range
std::string range_str = gopt.get_value<std::string>("range");
// 用 parse_string_to_value 解析多个变量
double xmin, xmax, ymin, ymax;
std::string cover_str;
utilsxx::parse_string_to_value(range_str, '/', true,
xmin, xmax, ymin, ymax, cover_str);
// 结果:xmin=100, xmax=1000, ymin=200, ymax=2000, cover_str="full_range"
std::cout << xmin << " " << xmax << " " << ymin << " " << ymax
<< " " << cover_str << std::endl;
7.2 显示所有参数
调试时查看所有已读取的参数:
gopt.show_options(); // 只显示已设置的参数
gopt.show_options(std::clog, true); // 显示所有参数(包括未设置的)
输出示例:
range: 100/1000/200/2000/full_range
interval: 10/10
weight: 120.12/232/114.2
model-file: test.txt
model-tag: model1,model2,model3
out-model: example.msh
======================
7.3 自定义分隔符
根据配置文件的格式灵活调整:
// YAML 风格(冒号分隔)
gopt.read_options("config.yaml", ":");
// INI 风格(等号分隔,分号注释)
gopt.read_options("config.ini", "=", ",", ';');
// 空格分隔(极简格式)
gopt.read_options("config.txt", " ");
7.4 多配置文件分层加载
实现类似"默认配置 + 用户配置"的覆盖机制:
// 先加载默认配置
gopt.read_options("default_config.txt");
// 再加载用户配置,同名参数会覆盖
gopt.read_options("user_config.txt");
因为 read_options() 对已存在的 key 会追加值(用连接符),要实现覆盖需要配合 clear():
// 方案:分开管理
gopt.read_options("default_config.txt");
auto defaults = gopt; // 保存默认值
gopt.clear();
gopt.read_options("user_config.txt");
// 然后手动合并...
7.5 与 CLI11 结合使用
utilsxx 同时集成了 CLI11 库用于命令行参数解析。两者可以互补:
// 1. 用 CLI11 解析命令行参数
CLI::App app("My Program");
std::string config_file;
app.add_option("-c,--config", config_file, "Config file");
CLI11_PARSE(app, argc, argv);
// 2. 用 getoption 解析配置文件
utilsxx::getoption gopt;
gopt.add_options({...}, {...});
gopt.read_options(config_file);
gopt.check_mandatory();
八、完整示例
以下是一个科学计算程序的完整配置解析示例:
#include "get_option.hpp"
#include "vector.hpp"
#include <iostream>
#include <string>
int main() try {
utilsxx::getoption gopt;
// 1. 声明所有参数
gopt.add_options({
"range", // 计算范围 (必填)
"interval", // 采样间隔 (必填)
"weight", // 权重参数 (必填)
"model-file", // 模型文件 (必填)
"model-tag", // 模型标签 (必填,可多值)
"out-model", // 输出模型
"save-model" // 保存模型
}, {
true, true, true, true, true, false, false
});
// 2. 设置互斥组:out-model 和 save-model 至少选一个
gopt.set_group(1, {"out-model", "save-model"});
// 3. 读取配置(可从文件或内存向量)
gopt.read_options("data/option_sample.txt");
// 4. 校验
gopt.check_mandatory();
gopt.check_group(1);
// 5. 显示配置
gopt.show_options();
// 6. 获取并使用参数
std::string model_file = gopt.get_value<std::string>("model-file");
std::cout << "Model file: " << model_file << std::endl;
// 获取多值权重
std::vector<double> weight = gopt.get_values<double>("weight", '|', '/');
std::cout << "Weights (" << weight.size() << "): ";
for (auto w : weight) std::cout << w << " ";
std::cout << std::endl;
// 获取多值标签
std::vector<std::string> tags = gopt.get_values<std::string>("model-tag", '|', ',');
std::cout << "Tags: " << tags << std::endl;
// 解析范围参数
double xmin, xmax, ymin, ymax;
std::string cover_str;
utilsxx::parse_string_to_value(
gopt.get_value<std::string>("range|Range"),
'/', true, xmin, xmax, ymin, ymax, cover_str);
std::cout << "Range: [" << xmin << ", " << xmax << "] x ["
<< ymin << ", " << ymax << "], mode: " << cover_str << std::endl;
return 0;
}
catch(utilsxx::error_handler &e) {
e.show();
return 1;
}
对应配置文件 option_sample.txt:
# this is an annotation
range = 100/1000/200/2000/full_range
interval = 10/10
weight = 120.12/232/114.2
model-file = test.txt
# you can insert empty lines too
model-tag = model1
model-tag = model2
model-tag = model3
out-model = example.msh
九、API 速查表
参数声明
| 方法 | 说明 |
|---|---|
add_option(name, mandatory, group) | 添加单个参数 |
add_options(names, mandatories) | 批量添加参数 |
set_group(gp, names) | 将参数归入互斥组 |
配置读取
| 方法 | 说明 |
|---|---|
read_options(filename, sep, conn, annotate) | 从文件读取 |
read_options(str_vec, sep, conn, annotate) | 从字符串向量读取 |
校验
| 方法 | 说明 |
|---|---|
check_mandatory() | 检查必填项(组内参数除外) |
check_group(gp) | 检查组内至少一个参数已设置 |
check_groups() | 检查所有组 |
查询
| 方法 | 说明 |
|---|---|
has_value(key, delimiter) | 检查参数是否已设置 |
get_value<T>(key, delimiter, sep) | 获取单个值(自动类型转换) |
get_values<T>(key, delimiter, sep) | 获取多值向量 |
show_options(out, show_all) | 显示所有参数 |
clear() | 清空所有参数 |
辅助函数
| 函数 | 说明 |
|---|---|
parse_key_value(str, sep, key, value) | 解析 key-value 字符串 |
十、设计哲学与最佳实践
10.1 何时使用 getoption?
| 场景 | 推荐方案 |
|---|---|
| 程序需要大量配置参数 | getoption |
| 参数有复杂的校验规则(必填、互斥) | getoption |
| 配置文件需要注释和空行 | getoption |
| 纯命令行工具,参数少 | CLI11 |
| 需要子命令支持 | CLI11 |
| 两者结合 | CLI11 解析命令行 + getoption 解析配置文件 |
10.2 命名建议
- 使用 kebab-case(短横线连接):
model-file、max-iterations - 同时注册 camelCase 别名:
model-file|modelFile - 避免使用下划线开头(可能与内部变量冲突)
10.3 错误处理
getoption 在以下情况会抛出 error_handler 异常:
- 必填参数未设置(
check_mandatory()) - 组内参数全部缺失(
check_group()) - 读取了不存在的 key(
get_value()) - 初始化列表长度不匹配(
add_options())
建议始终用 try-catch 包裹:
try {
// ... 配置解析代码 ...
}
catch(utilsxx::error_handler &e) {
e.show(); // 打印友好的错误信息
return 1;
}
结语
getoption 以声明式的设计简化了 C++ 程序的配置管理。你只需声明"程序需要什么参数",剩下的解析、校验、类型转换都由它自动完成。配合别名支持、分组校验和多值解析,它能优雅地处理从简单脚本到复杂科学计算程序的各种配置需求。
项目地址:utilsxx