准备:配置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_cast
return 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;
}