问题?

在使用 JsonCpp 相关的库对配置数据进行读写时,有可能会出现这样的问题,首先写入数据到文件:

// 写入数据
uint32_t width = 1920;
uint32_t height = 1080;
uint32_t fps = 25;

Json::Value value;
value["width"] = width;
value["height"] = height;
value["fps"] = fps;

... // 写入到文件

然后创建一个一模一样的变量来和文件中读取出来的数据做比对:

// 从文件中读取的变量
Json::Value value_from_file;
... // 读取文件

uint32_t width = 1920;
uint32_t height = 1080;
uint32_t fps = 25;

// 创建一个相同kv变量
Json::Value value;
value["width"] = width;
value["height"] = height;
value["fps"] = fps;

// 两者进行比对
if (value == value_from_file) {
printf("equal!\n");
} else {
printf("not equal!\n");
}

然后在输出的时候你会发现,这两个变量其实是不等的,那么为什么 kv 一致的情况下判断出来却不相等呢?
查询 JsonCpp 源码后可知晓:

/** \brief Type of the value held by a Value object.
*/
enum ValueType {
nullValue = 0, ///< 'null' value
intValue, ///< signed integer value
uintValue, ///< unsigned integer value
realValue, ///< double value
stringValue, ///< UTF-8 string value
booleanValue, ///< bool value
arrayValue, ///< array value (ordered list)
objectValue ///< object value (collection of name/value pairs).
};

JsonCpp 中对整型数据进行了细分,然而 Json 并没有特殊的规范针对文件中的整型进行有无符号的区分,所以 JsonCpp 读取到的整型均以有符号的方式来读取,这就导致了上面的问题,即 kv 虽然相等了,但是 v 的类型不相等
这个情况同时也导致了某些实现监听配置变动时,在回调中对新老值的比对上出现问题:

...
void XX::OnCfgChanged(const listener_id id, const Json::Value& old_value, const Json::Value& new_value) {
...
// 导致不应该的业务逻辑被触发
if (old_value["resolution"] != new_value["resolution"]) {
...
}
...
}
...

如只需要简单的使用,那么不需要在意很多,但对于复杂的项目来说就应该尽量避免这样实现

如何更好的设计?

其实可以将配置视为一个对象来进行设计,以对象来管理资源一直是 C++ 的处理方式,这样就可以定义好 value 的类型:

"obj": {
"text": "_text_",
"num": 1
}
struct Obj {
std::string text;
int32_t num;
};

这样既可以确认对应配置所具有的 kv 也可以保证类型安全。当然我们的目的是将 Json 与目标值进行转换,所以可以基于 Json 库来定义接口来编写,这里使用 nlohmann 库来做为 Json 的转换:

namespace tools {
namespace var {

class Base {
public:
virtual ~Base() = default;
/**
* @brief 解释Json对象
*/
virtual void FromJson(const nlohmann::json& json) = 0;
/**
* @brief 转换为Json对象
*/
virtual nlohmann::json ToJson() = 0;
/**
* @brief 序列化, 调用ToJson进行转换, 再通过库函数转换为字符串
* @return 返回Json字符串
*/
virtual std::string ToJsonString() {
return nlohmann::to_string(ToJson());
}
/**
* @brief 反序列化, 调用库进行解析, 再通过调用FromJson进行转换
* @param json_str Json字符串
* @throw 解析失败抛出nlohmann::json::parse_error
* @return 返回是否成功反序列化
*/
virtual void FromJsonString(const std::string& json_str) {
FromJson(nlohmann::json::parse(json_str));
}
};

} // var
} // tools

那么 Obj 就可以继承 tools::var::Base 进行实现:

using namespace tools;

// 继承VarBase进行实现
struct Obj : public var::Base {
std::string text;
int32_t num{};

void FromJson(const nlohmann::json& json) override {
// 目标值, 键值, 不存在时的默认值
const auto cvt = [&](auto& dst, const char* key, auto def_value) {
dst = json.contains(key) ? json[key].get<std::remove_reference_t<decltype(dst)> >() : def_value;
};
cvt(text, "text", "(null)");
cvt(num, "num", 0);
}

nlohmann::json ToJson() override {
nlohmann::json json;
json["text"] = text;
json["num"] = num;
return json;
}
};

这样对于调用者来说就无需关注如何使用 Json 库转换出自己想要的数据,同时也可以在转换的函数里对数据进行对应的数据解析转换成对应的目标成员值

总结

其实无论是 Json 还是 Yaml 或者 ini 等其他配置格式都可以考虑这样的实现方式,毕竟这样在大型的项目中是十分有利于其他模块的对接和处理,同时也可以减少代码互相耦合的情况