cpp配置系统(YAML)的设计与实现()
问题引入
我们为什么需要一个配置系统,配置系统需要具备什么功能? 1. 整个系统的实现中是需要有很多配置的:比如说系统将要开启的端口号20,80,还是8080,又或者日志保存的目录,我需要什么日志器。再或者我可能需要它读取配置文件完成相应事件,比如dockerfile。又或者读取配置,比如vscode的json文件。 那你可能会想?为什么不再代码里实现呢,比如
const int system_port = 80 ; //server statrt at this port
由于我们书写的是一个库的代码,当代码量小的时候这么写也不是不行。但当代码量激增后,你要怎么去查找哪个变量在哪个文件,修改一个变量还好,多个系统的多个配置呢?这样子想必是低效的。而且修改后,要重新编译,不管什么变量,服务都需要要重启。 基于以上问题,我们对配置系统有如下的要求: 1. 我们要事先规定,哪些变量是被配置系统支配的,比如端口号,超时时长设定,用户最大数量。无论,你是否书写配置文件,它都是存在的,“约定 先于 配置”。我们使用变量的名称(可以是自己设定的)作为key,在约定时构建一个哈希表,这样后来配置的时候就可以查表,至于value存什么后面再说。 2. 当我的系统需要这个值的时候,我们呢不需要检查这个值是否被配置过,配置系统应该隐藏配置的过程,其他系统只用使用配置变量的value。 比如说:
// 我们预期的样子
server->start_service(Config->getValue());
//而不是
if(xxx_changed){
Config->update();
server->start_service(Config->getValue());
- 我们要存的值的类型时多样的,我们的哈希表是这样的std::unordered_map<:string>,由于value_type的多样化,我们无法直接使用value_type构建一个哈希表。
如何解决第三个问题的思考
对于多种类型,我们第一个想到的肯定是模板类
template <typename T>
class ConfigVar{
public:
T getValue() const { return m_val;};
private:
std::string m_name; // key
T m_val;
这是我们期望的配置变量的写法,这是它应该具备的最基本的功能。
// 构建哈希表
std::unordered_map<std::string,std::shared_ptr<ConfigVar<T > > > m_config_table;
很显然这样写不行,我们又开始了新一轮的思考 我们要固定value的type,它肯定不能是内置类型比如int,double等,它是一个我们的自建类型,把其中类型不会变的量保留下来,如下:
class ConfigVarBase{
public:
private:
std::string m_name;
std::unordered_map<std::string,ConfigVarBase >
这样是可以,但我们的值怎么办?我们怎么通过这个类获取我们所需要的值? value_type的泛型,我们逃不开模板。通过模板获取值,我们一般的思维可能是这样的:
template<class T>
T getValue(){
return m_val;
由于我们的ConfigVarBase里没有保存任何的关于value的信息(仅有一个std::string m_name),让用户传参,我们也无法管理。这里引入这个系统第一个设计技巧:
模板子类继承非模板基类
// 基类需要定义virtual方法,这里略
template <typename T>
class ConfigVar : public ConfigVarBase{
public:
T getValue();
private:
T m_val;
这个技巧很常见,我在使用workflow的list构建我自己的list的时候也用到了,只不过略有区别,见后文的问题。如果你对面向对象有一定了解,你就可以知道所谓动态绑定,发生在参数是基类的指针或引用时,子类可以完成向基类的转换,ConfigVar -> ConfigVarBase,通过这个过程把配置变量存入哈希表中。那么还是那个问题?我们怎么取出呢,怎么让基类转换为子类? 参考C++ Pimer P730 运行时类型识别: 运行时类型识别(run-time type identification RTTI) : 1. typeid运算符,用于返回表达式的类型 2. dynamic_cast 运算符,用于将基类的指针或引用安全地转换成派生类地指针或引用
dynamic_cast运算符
dynamic_cast<type*>(e)
当然还有&,&&两种,type必须是一个类类型,并且通常情况下该类型有虚函数。 e的类型必须符合以下三个条件中的任意一个: 1. e的类型时目标type的公有派生类 2. e的类型时type的共有基类 3. e的类型就是type 如果指针类型,转换失败返回0.引用类型,则抛出bad_cast异常。 至于如何裸指针如何实现,由于欠缺对C++继承的具体实现的了解,我还不能实现。
当然这里可以留个思考题,是我前几天读C的代码的时候看到的:
struct list_node {
struct list_node* next;
template <typename T>
struct m_list_node{
struct list_node m_node;
T m_val;
问题在于,我们的链表只管理list_node ,而非m_list_node,我们如何通过一个list_node* 获取m_list_node进而取得m_val。 至此我们已经解决了如何进行配置系统最基本的配置变量管理的设计思路。
配置文件类型
现在配置文件类型由很多yaml,json,甚至可以是txt,我们只需要选取我们喜欢,或者说比较熟悉的文件类型就可以,我这边看的教程用的ymal,所以我也用的yaml,你如果喜欢用json,那就用开源的cpp的json库就好了,我相信它也一定会提供ymal-cpp所对应的功能。
序列化与反序列化
文件->配置系统,是反序列化; 配置系统->文件,是序列化。 无论yaml或json,甚至于你自定义类型,它都存在规范,我们需要利用这种规范实现我们的序列化和反序列化。
yaml-cpp简单介绍(我们该怎么开启一个开源库)
当你需要一个第三方库,比如你可能需要一个网络库,一般是在github上找到的,我们第一件事情呢,就是仔细阅读它的readme.md(由此可见一个readme.md对一个开源项目的重要新),如果我们读完发现,它的写法或者设计不符合自己习惯,甚至于它的功能不行,我们就会换到下一个库。
README组成
- 用法:里面由Tutorial(这对于一个开源项目来说也是很重要的东西)
- 安装方法: Linux下不到十分钟搞定,win下折腾一下午我也没装上。。。
- API手册
第一步肯定是,安装它的安装指示,大部分cpp开源库都是camke,在linux下,连接什么的,基本都没什么障碍,甚至于一条命令就可以完成安装,使用的时候连接上就行,cpp生态好像也没那么差。
用法(Tutorial)
第一个例子,注释是我写的
// 取名不错,从文件加载YAML,参数名称 是filename,也是一目了然
// 返回了一个YAML::Node类型的对象,用于保存从yaml文件中读取到的所有信息,读取失败会抛出异常
// 反序列化
YAML::Node config = YAML::LoadFile("config.yaml");
// 布尔,<<重载(用于序列化),这个后面写多了就知道了
if (config["lastLogin"]) {
std::cout << "Last logged in: " << config["lastLogin"].as<DateTime>() << "\n";
// 获取数据
const std::string username = config["username"].as<std::string>();
const std::string password = config["password"].as<std::string>();
login(username, password);
config["lastLogin"] = getCurrentDateTime();
// 序列化
std::ofstream fout("config.yaml");
fout << config;
展示了序列化,反序列化,数据的获取。 简单的解析和结点编辑:
// 这个用法也很重要,反序列化的另一种方式,文件也不过是一个大一点的string
YAML::Node node = YAML::Load("[1, 2, 3]");
assert(node.Type() == YAML::NodeType::Sequence);
assert(node.IsSequence()); // a shortcut!
节点是有类型的(sequence和哈希表) 用法和STL的vector和map差不多: 接下来就是它给出的证明:
YAML::Node primes = YAML::Load("[2, 3, 5, 7, 11]");
// 可以自己写几几个yml跑一跑
// 类vector用法
for (std::size_t i=0;i<primes.size();i++) {
std::cout << primes[i].as<int>() << "\n";
// 迭代器
for (YAML::const_iterator it=primes.begin();it!=primes.end();++it) {
std::cout << it->as<int>() << "\n";
primes.push_back(13);
assert(primes.size() == 6);
// map用法
YAML::Node lineup = YAML::Load("{1B: Prince Fielder, 2B: Rickie Weeks, LF: Ryan Braun}");
for(YAML::const_iterator it=lineup.begin();it!=lineup.end();++it) {
std::cout << "Playing at " << it->first.as<std::string>() << " is " << it->second.as<std::string>() << "\n";
lineup["RF"] = "Corey Hart";
lineup["C"] = "Jonathan Lucroy";
assert(lineup.size() == 5);
如果你对cpp的vector和map有一定基础的话,上述代码可以说没有负担的。大概就还是在展示用法,后面有一些小提示,慢慢阅读。 node的类型node.Type(): 1. Null 2. Scalar 3. Sequence 4. Map 5. Undefined 可以使用node.IsXXX()来判断结点类型
后面就是很多的用法举例,不是很难,但读完整个简单的介绍,会对它的用法有个大概了解,基本上可以投入使用了。你会感受到用轮子的快乐,这是我用的第一个第三方库hhh,第二个是sogou的workflow。 最后一个例子很重要,说的是,如果你需要你自定义的对象类型和yaml-cpp交互,你可以特化 YAML::convert<> template class。例子如下:
// 自定义类型
struct Vec3{
// 原文说了一句需要重载==,但下面的例子并没有用到==,也不是很懂
double x,y,z;
namespace YAML{
// 特化
template<>
struct convert<Vec3> {
static Node encode(const Vec3& rh3){
Node node;
node.push_back(rhs.x);
node.push_back(rhs.y);
node.push_back(rhs.z);
static bool decode(const Node& node,Vec3& rhs){
if(!node.IsSequence() || node.size() != 3){
return false;
rh3.x = node[0].as<double>();
rh3.y = node[1].as<double>();
rh3.z = node[2].as<double>();
return true;