准备:配置boost库
概述:boost库是一个优秀且可以移植的开源C++库,是C++标准委员会库工作成员发起,是对STL的延伸和扩充,设计理念和STL比较接近,利用泛型编程达到利用率最大化,有些内容经常成为下一代C++标准库的内容
1. 下载压缩包
https://www.boost.org/ 进入boost官网下载压缩包,下载的是boost_1_77_0.tar.gz v1.77最新版本
tar -xzvf [boost_1_77_0.tar.gz](https://boostorg.jfrog.io/artifactory/main/release/1.77.0/source/boost_1_77_0.tar.gz) 解压后得到文件夹:
2. 执行脚本文件
cd boost_1_77_0 进入文件夹后执行脚本sudo ./bootstrap.sh
3. 执行安装
结束后继续执行sudo ./b2 install 完成安装
· 类关系

1. ConfigVarBase 类封装 (接口类)
1.1 成员变量
class ConfigVarBase{......protected:/// 配置项名称std::string m_name;/// 配置项描述std::string m_description;};
1.2 接口
1.2.1 构造函数
/*** @brief 配置项虚基类构造函数* @param[in] name 配置项名称* @param[in] description 配置项描述*/ConfigVarBase(const std::string & name, const std::string &description = ""):m_name(name),m_description(description){//把配置项的命名限制在小写范围内 输入的大写全部转为小写std::for_each(m_name.begin(), m_name.end(), [&](char &c){ if(c >= 'A' && c <= 'Z') c += 32;});}
· 对输入的Yaml文件key值(配置项名称),限定在小写范围内
做法一:算法
transform(sourceBeg, sourceEnd, destBeg, op)——>针对源区间
[sourceBeg, sourceEnd)中的每一个元素调用:op(elem) 并将返回结果拷贝写到以destBeg起始的目标区间内。std::transform(m_name.begin(), m_name.end(), m_name.begin(), tolower);做法二:算法
for_each(_InIt _First, _InIt _Last, _Fn _Func)——->针对源区间
[InIt _First, _InIt _Last)中的每一个元素调用:Fn _Func(elem) 采用引用传递,且函数一般没有返回值,通过引用的方式改变传入参数的值。std::for_each(m_name.begin(), m_name.end(), [&](char &c){ if(c >= 'A' && c <= 'Z') c += 32;}二者区别:
transfrom的效率不高,通过拷贝函数返回值的方式对目的区间进行赋值,但操作函数不采用引用传递,比较灵活;for_each的效率较高,因为操作函数通过引用传递的方式改变操作元素的值,不存在拷贝,但是比较死板。
1.2.2 虚接口
/*** @brief 将任意输入内容转换为string类型* @return std::string*/virtual std::string toString() = 0;//用于解析 初始化/*** @brief 从string类型转换为其他所需类型* @param[in] val 传入的字符串数据* @return true 转换成功* @return false 转换失败*/virtual bool fromString(const std::string &val) = 0;/*** @brief 获取配置项类型名称* @return std::string*/virtual std::string getTypeName() const = 0;
1.2.3 常用接口
/*** @brief 获取配置项名称* @return const std::string&*/const std::string & getName() const {return m_name;}/*** @brief 获取配置项描述* @return const std::string&*/const std::string& getDescription() const {return m_description;}
2. LexicalCast 类封装
目的:完成对基本类型、复合类型、自定义类型与string字符串类型之间的相互转换。只需要将待转化的类型进行一个模板偏特化就能实现,无需重复实现相同形式的代码,尽量复用现有代码。
详情见:配置系统开发(二)篇章
- 技术点:利用模板偏特化、仿函数,尽可能的去简化这个模板类的使用。通过制定待定参数
F、T数据类型完成模板的偏特化。 ```cpp /**- @brief 类型转换,只完成基础类型转换 type F —-> type T
- @tparam F 待转换类型
- @tparam T 转换后类型
*/
template
class LexicalCast { public: T operator()(const F& v) {
}return boost::lexical_cast<T>(v);
};
<a name="umO8B"></a># 3. ConfigVar 类封装目的:实现`ConfigVarBase`类的接口,并且置为模板类,方便自由的传入参数类型,而后调用 2. 中封装中好的万能转换去完成各种类型的序列化和反序列化,能使得配置项的存储和落盘较为便捷<a name="hVb94"></a>## 3.1 成员变量```cpp/*** @brief 配置项类* @tparam T 维护配置项的数据类型* @tparam FromStr 从字符串转为T类型采用的模板类* @tparam ToStr 从T类型转为字符串类型采用的模板类*/template<class T, class FromStr = LexicalCast<std::string, T>,class ToStr = LexicalCast<T, std::string> >class ConfigVar :public ConfigVarBase{......private:/// 配置项数据T m_val;/// 配置项所绑定的回调函数容器std::map<uint64_t, on_change_cb> m_cbs;/// 读写锁MutexType m_mutex;};
3.2 接口
3.2.1 构造函数
/*** @brief 配置项类构造函数* @param[in] name 配置项名称,作为key值* @param[in] default_value 配置项的默认值* @param[in] description 配置项的描述*/ConfigVar(const std::string &name, const T& default_value,const std::string description = ""):ConfigVarBase(name, description),m_val(default_value){}
3.2.2 重写接口
3.2.2.1 toString()
功能:将配置项数据内容转换为string类型。通过ToStr()接口自动的去匹配符合的类模板,再调用目标类模板中的函数对象完成转换操作
/*** @brief 将配置项数据内容转换为string类型* @return std::string*/std::string toString() override{try{//return boost::lexical_cast<std::string>(m_val);//加读锁MutexType::ReadLock lock(m_mutex);//调用函数对象return ToStr()(m_val);}catch (std::exception &e){KIT_LOG_ERROR(KIT_LOG_ROOT()) << "ConfigVar::toString exception" << e.what() << " convert:" << std::string(typeid(m_val).name()) << " to string";}return "";}
3.2.2.2 FromString()
功能:将配置项从string类型转换为其他所需类型。通过FromStr()接口自动的去匹配符合的类模板,再调用目标类模板中的函数对象完成转换操作,将转换好的值设置到配置项中。
/*** @brief 将配置项从string类型转换为其他所需类型* @param[in] val 传入的字符串数据* @return true 转换成功* @return false 转换失败*/bool fromString(const std::string &v) override{try{setValue(FromStr()(v));}catch(std::exception &e){KIT_LOG_ERROR(KIT_LOG_ROOT()) << "ConfigVar::fromString exception " << e.what() << " convert: string to " << std::string(typeid(m_val).name());}return false;}
3.2.2.3 getTypeName()
功能:获取配置项类型名称
/*** @brief 获取配置项类型名称* @return std::string*/std::string getTypeName() const override { return typeid(T).name();}
3.2.3 setValue()/getValue()(核心)
功能:设置/获取配置项的数据。setValue()函数在发生数据更新时,会负责触发配置项上绑定的回调函数。这种机制的应用场景比较重要:游戏数值更新触发对应事件、配置项热更新等。
/*** @brief 获取配置项,传入什么类型就返回什么类型* @return const T*/const T getValue(){MutexType::ReadLock lock(m_mutex);return m_val;}/*** @brief 给配置项设置具体的数值,从.yaml读到信息后同步到内存中来* @param val*/void setValue(const T& val){{//加读锁MutexType::ReadLock lock(m_mutex);//原来的值是否和新值一样 就不用设置if(val == m_val)return;//一个配置项可设置多个回调函数进行触发for(auto &x : m_cbs)x.second(m_val, val); //调用回调函数执行相关操作}//出作用域 读锁解锁//加写锁MutexType::WriteLock lock(m_mutex);m_val = val;}
3.2.4 添加/删除/获取/清除配置项上的回调函数
/*** @brief 为配置项绑定一个回调函数* @param[in] cb 指定的回调函数* @return uint64_t 返回一个唯一标识回调函数的key值*/uint64_t addListener(on_change_cb cb){//内部自己生成一个key(id)来唯一标识回调函数 key不能重复static uint64_t cb_fun_id = 0;//加写锁MutexType::WriteLock lock(m_mutex);++cb_fun_id;m_cbs[cb_fun_id] = cb;return cb_fun_id;}/*** @brief 删除配置项上的某一回调函数* @param[in] key 唯一标识回调函数的key值*/void delListener(uint64_t key){MutexType::WriteLock lock(m_mutex);m_cbs.erase(key);}/*** @brief 获取配置项上的某一回调函数* @param[in] key 唯一标识回调函数的key值* @return on_change_cb*/on_change_cb getListener(uint64_t key){MutexType::ReadLock lock(m_mutex);auto it = m_cbs.find(key);return it == m_cbs.end() ? nullptr : it->second;}/*** @brief 清除配置项上的所有回调函数*/void clearListener(){MutexType::ReadLock lock(m_mutex);m_cbs.clear();}
4. Config 类封装
目的:对所有的配置项进行统一管理,更加快捷的创建、访问配置项。
4.1 成员变量
这样写的原因是程序开始运行进入main()函数之前,要保证容器和锁已经被初始化好,否则有可能出现有的文件中已经发生调用,但是这些资源还没初始化的情况。
/*** @brief ConfigVar的管理类* @details 提供便捷的方法创建/访问ConfigVar*/class Config{public://将配置项容器重命名 方便使用typedef std::map<std::string, ConfigVarBase::ptr> ConfigVarMap;typedef RWMutex MutexType;......private:/*** @brief 获取配置项容器,由于初始化顺序问题写成static函数形式* @return ConfigVarMap&*/static ConfigVarMap& GetDatas(){static ConfigVarMap m_datas;return m_datas;}/*** @brief 获取读写锁* @return MutexType&*/static MutexType& GetMutex(){static MutexType m_mutex;return m_mutex;}};
4.2 接口
4.2.1 LookUp()(核心)
功能:查询配置项
/*** @brief 查询配置项,如果没有就会创建* @tparam T 配置项的参数类型* @param[in] name 配置项名称* @param[in] default_value 配置项默认值* @param[in] description 配置项描述* @return ConfigVar<T>::ptr*/template<class T>static typename ConfigVar<T>::ptr LookUp(const std::string &name, const T& default_value,const std::string &description = ""){//加写锁MutexType::WriteLock lock(GetMutex());//判断查询的配置项是否存在auto it = GetDatas().find(name);if(it != GetDatas().end()) //配置项存在{auto temp = std::dynamic_pointer_cast<ConfigVar<T> >(it->second);if(temp){return temp;}else{KIT_LOG_ERROR(KIT_LOG_ROOT()) << "LookUp name:" << name << " exists but not =" << typeid(T).name() << " real-type=" << it->second->getTypeName();return nullptr;}}/*配置项不存在就创建*///排除一些非法的配置项名称if(name.find_first_not_of("abcdefghijklmnopqrstuvwxyz_.0123456789") != std::string::npos){KIT_LOG_ERROR(KIT_LOG_ROOT()) << "LookUp name invalid " << name;//抛出无效参数异常throw std::invalid_argument(name);}typename ConfigVar<T>::ptr v(new ConfigVar<T>(name, default_value, description));GetDatas()[name] = v;return v;}/*** @brief 查询配置项,如果没有返回nullptr* @tparam T 配置项的参数类型* @param[in] name 配置项名称* @return ConfigVar<T>::ptr*/template<class T>static typename ConfigVar<T>::ptr LookUp(const std::string &name){//加读锁MutexType::ReadLock lock(GetMutex());auto it = GetDatas().find(name);if(it == GetDatas().end()){return nullptr;}//当转化对象为智能指针时使用dynamic_pointer_castreturn std::dynamic_pointer_cast<ConfigVar<T> >(it->second);}
4.2.2 LoadFromYaml()(核心)
功能:从yaml结点中加载配置项数据。一般在调用YAML库的接口YAML::LoadFile(文件路径)后从文件中读取数据构造成一个yaml结点,再调用该接口从原始yaml结点中拆解出对应的配置项,依次对需要的配置进行一个设置。
/*** @brief 从yaml结点中加载配置项数据* @param[in] node 带有数据的yaml结点*/static void LoadFromYaml(const YAML::Node & node);void Config::LoadFromYaml(const YAML::Node &root){std::list<std::pair<std::string, YAML::Node> > all_nodes;ListAllMember("", root, all_nodes);for(auto &x : all_nodes){std::string key = x.first;//空字符串就跳过if(!key.size())continue;//将配置项名称中的大写字母全部转为小写字母std::for_each(key.begin(), key.end(), [&](char &c){ if(c >= 'A' && c <= 'Z') c += 32;});//寻找是否有已经约定的配置项,如果有就进行修改ConfigVarBase::ptr v = LookUpBase(key);if(v){//如果当前yaml结点是标量型数据直接进行转化if(x.second.IsScalar()){v->fromString(x.second.Scalar());}else //如果当前yaml结点是复合数据需要以流的方式传入参数{std::stringstream ss;ss << x.second;v->fromString(ss.str());}}}}
· 辅助函数:ListAllMember()
功能:将yaml结点进行序列化,把每一个配置项扁平化
/*** @brief 将yaml结点数据序列化* @param[in] prefix 格式控制+key值组合* @param[in] node 带有数据的yaml结点* @param[out] output 存储序列化结果的容器*/static void ListAllMember(const std::string& prefix, const YAML::Node& node, std::list<std::pair<std::string, YAML::Node> >&output){//检查key值的合法性if(prefix.find_first_not_of("abcdefghijklmnopqrstuvwxzy._0123456789") != std::string::npos){KIT_LOG_ERROR(KIT_LOG_ROOT()) << "Config invauld nmae:" << prefix << ":" << node;throw std::invalid_argument(prefix);}//将 key:Node 存储到容器中output.push_back({prefix, node});//如果为Map类型 递归处理if(node.IsMap()){for(auto it = node.begin();it != node.end();++it){//进行递归ListAllMember(prefix.size() ? prefix + '.' + it->first.Scalar() : it->first.Scalar(), it->second, output);}}}
4.2.3 LookUpBase()
功能:返回配置项的基类指针
/*** @brief 返回配置项的基类指针* @param[in] name 配置项名称* @return ConfigVarBase::ptr*/static ConfigVarBase::ptr LookUpBase(const std::string& name);ConfigVarBase::ptr Config::LookUpBase(const std::string& name){//加读锁MutexType::ReadLock lock(GetMutex());auto it = GetDatas().find(name);return it == GetDatas().end() ? nullptr : it->second;}
4.2.4 Visit()
功能:轮询对所有配置项进行某个操作
/*** @brief 轮询对所有配置项进行某个操作* @param[in] cb 指定一个函数去操作配置项*/static void Visit(std::function<void(ConfigVarBase::ptr)> cb);void Config::Visit(std::function<void(ConfigVarBase::ptr)> cb){MutexType::ReadLock lock(GetMutex());ConfigVarMap& m = GetDatas();for(auto &x : m){cb(x.second);}}
boost库知识:万能转换器 boost::lexical_cast<>()
功能:统一接口实现不同类型间的相互转换,即:万能转换
使用样例:
int a;//std::string s = "123.12"; //如果使用该字符串转换,a无法成功转换std::string s = "123";a = boost::lexical_cast<int>(s);std::cout << a << std::endl;std::string b = boost::lexical_cast<std::string>(a);std::cout << b << std::endl;double c = boost::lexical_cast<double>(s); //传入"123.12"能够转换成功std::cout << c << std::endl;
和传统库函数的区别,C语言库的atoi、itoa 以及C++库的stringstream:
- C语言库的
atoi、itoa都是单向转换,没有双向转换,不同类型间转换需要依赖不同的接口函数 - 仅仅支持基本数据类型的转换,
int、double等 - C++中
stringstream可用性强但是可读性差,使用门槛高,对于简单转换,太重。
注意:如果对转换的精度有要求应该使用stringstream。数值之间转换最好使用boost::numberic
C\C++知识点补充复习:dynamic_pointer_cast<>()
都是实现基类和派生类之间安全转换。dynamic_pointer_cast用于转换对象为智能指针的时候;dynamic_cast用于转换对象为普通指针或引用的时候。
C\C++知识点补充复习:transform()算法
功能:针对源区间**[**sourceBeg, sourceEnd**)**中的每一个元素调用操作:op(elem) 并将返回结果拷贝写到目标区间内。
transform(sourceBeg, sourceEnd, destBeg, op)transform(sourceBeg, sourceEnd, destBeg, destEnd, op)
C\C++知识点补充复习:find_first_not_of()算法
功能:正向查找在原字符串中第一个与指定字符串(或字符)中的任一字符都不匹配的字符,返回它的位置。若查找失败,则返回string::npos。(npos定义为保证大于任何有效下标的值。)
使用地方:判断字符串name中是否包含规定之外字符,如果包含就要throw出异常
if(name.find_first_not_of("abcdefghijklmnopqrstuvwxyz_.0123456789") != std::string::npos){KIT_LOG_ERROR(KIT_LOG_ROOT()) << "LookUp name invalid " << name;throw std::invalid_argument(name);}
手写算法替代:
//判断传入的name是否合法 只能包含"abcdefghijklmnopqrstuvwxyz_.0123456789"这些字符bool isRight(const std::string &str){for(auto &x : str){if((x < 'a' || x > 'z' )&& !isdigit(x) && x != '.' && x != '_')return false;}return true;}
