背景:
在项目里经常遇到对象和json字符串的相互转换这类问题,在大多数程序里,一般这个问题都比较有比较好的解决方法,往往一个函数搞定。但是到了c++这边却需要我们手撸json库一个一个字段初始化/序列化。如果这时候有一个函数 可以一行代码 unmarshal /marshal 对象岂不是很方便?本文以jsoncpp库为基础,设计这样一个可以支持这种功能的函数,下面进入正题~
设计思路
以unmarshal 为例,我们最终的函数是打造两个这样的模板函数 :
一个从string 的josn直接反序列化对象,一个从jsoncpp库的json对象,反序列化对象。
template<typename T> bool Unmarshal(T& obj,const string& json_str);
template<typename T> bool Unmarshal(T& obj,const Json::Value& json_obj_root);
由于json是具有自递归结构的,所以在设计时,应该也是以递归的方式解决复杂的组合类,我们可以简单的把程序中的变量分为下面几类:
这样我们只需要把这几个场景的 Unmarshal实现了,整体的Unmarshal也就实现了。模板设计的类图应该和我们的分类相对应:
在实现中要注意以下几点:
1、每个分类的Unmarshal模板具有排他性,也就是说基本类型在编译期间只能匹配解析基本类型的模板
2、由于1的保证和这些模板函数名称都是Unmarshal,所以他们之间可以相互嵌套调用对方。
3、指针、和原生数组会涉及到空间分配和长度检测会使得情况变得复杂,本次设计支持的范围不包含对指针、原生数组的支持。
匹配基本类型的Unmarshal模板
//只能解析基本类型 int long bool float double string 的一组模板
/*
* 其实可声明为重载函数,声明为模板只是为了可以放在头文件中
* 不能声明为 template <typename T> bool Unmarshal(int& obj,const Json::Value &root);是因为
* 在编译时编译器不能推断出T的类型,导致编译失败.所以将模板类型列表设置为template <int = 0> (Nontype
* Parameters) 此处int并无实际意义
*/
template <int = 0>
inline bool Unmarshal(int& obj,const Json::Value &root){
if(!root.isIntegral())
return false;
obj = root.asInt();
return true;
}
template <int = 0>
inline bool Unmarshal(long& obj,const Json::Value &root)
.....
匹配stl容器/其他第三方类库的Unmarshal模板
//只能匹配 vector<T> map<string,T> map<long,T> map<int,T> 的一组模板
//vector
template <typename T>
bool Unmarshal(vector<T>& obj,const Json::Value& root){
if(!root.isArray())
return false;
obj.clear();
bool ret = true;
for(int i=0;i<root.size();++i){
T tmp; //类型T要含有T()构造函数
if(!Unmarshal(tmp,root[i])) //递归调用Unmarshal函数
ret = false;
obj.push_back(tmp);
}
return ret;
}
//map key:string
template <typename T>
bool Unmarshal(map<string,T>& obj,const Json::Value& root){
...
}
//map key:long
template <typename T>
bool Unmarshal(map<long,T>& obj,const Json::Value& root){
...
}
//map key:int
template <typename T>
bool Unmarshal(map<int,T>& obj,const Json::Value& root){
...
}
匹配自定义struct/class的Unmarshal模板
实现一组只能匹配自己定义的struct/class 就需要我们定义的对象有一些特殊的标志,才能被模板函数识别。在这里选择给我们自己定义的类都实现public的unmarshal方法(实现方式后面讲),这样当编译时发现一个对象含有 public的 unmarshal方法时,就知道使是我们自己定义的类,然后就调用特定的模板函数,这里用到到了一个C++的语法 **SFINAE(**Substitution Failure Is Not An Error) 和std库中enable_if
我们先来看一下C++ std库中 enable_if 的简要实现:
// 版本1 一个空的enable_if 结构体
template <bool, class _Tp = void>
struct enable_if {};
// 版本2 是版本1第一个参数为true的特例化实现,这个版本的enable_if含有 type类型
template <class _Tp>
struct enable_if<true, _Tp> {typedef _Tp type;};
int main(){
enable_if<true,int>::type a = 3; //匹配版本2,相当于 int a = 3
enable_if<false,int>::type b = 3; //匹配版本1,enable_if{}没有type类型,触发编译错误
}
**SFINAE 准则就是匹配失败并不是错误,**如果编译器在匹配一个模板时引发了错误,这时候编译器应当尝试下一个匹配,而不应该报错中止。利用这条规则和enable_if,解析我们自己struct/class Umarshal模板设计如下:
// 检测一个类 是否含有非静态非重载的unmarshal方法
template<typename T>
struct TestUnmarshalFunc {
//版本1
template<typename TT>
static char func(decltype(&TT::unmarshal));
//版本2
template<typename TT>
static int func(...);
/*
* 如果类型T没有unmarshal方法,func<T>(NULL)匹配版本1时会产生错误,由于SFINAE准则,只能匹配版本2
* 的func,此时返回值4个字节,has变量为false.反之 has变量为true
*/
const static bool has = (sizeof(func<T>(NULL)) == sizeof(char));
};
//如果对象自身含有 unmarshal 方法,则调用对象的unmarshal.否则会因SFINAE准则跳过这个版本的Unamrshal
template <typename T,typename enable_if<TestUnmarshalFunc<T>::has,int>::type = 0>
inline bool Unmarshal(T& obj,const Json::Value &root){
return obj.unmarshal(root);
}
好了,至此我们对三种基本类型的Umarshal函数设计好了,这时候任意一个T类型 在调用Unmarshal时,最终会与上面三种其中一个匹配。json 为string的可以利用上面的Unmarshal再封装一个版本:
template <typename T>
bool Unmarshal(T& obj,const string &s){
Json::Reader reader;
Json::Value root;
if(!reader.parse(s,root))
return false;
return Unmarshal(obj,root);
}
接下来我们看如何在自定义的类中实现unmarshal函数:
//假设有一个People对象,有3个field需要反序列化,根据上面的要求,可能需要我们自己编写unmarshal如下
struct People{
bool sex;
int age;
string name;
//尽力解析每个field,只有全部正确解析才返回true
bool unmarshal(const Json::Value &root){
bool ret = true;
if(!Json::Unmarshal(sex,root["sex"])){
ret = false;
}
if(!Json::Unmarshal(age,root["age"])){
ret = false;
}
if(!Json::Unmarshal(name,root["name"])){
ret = false;
}
return ret;
}
};
显然如果field数量很多,就很麻烦,而且解析每个field时,代码格式非常相似,是否存在一个宏可以自动生成呢?答案是肯定的。talk is cheap,show me the code ,上代码!
struct People{
bool sex;
int age;
string name;
//用JSON_HELP宏把需要序列化的field传进去,就自动在类里生成unmarshal、marshal函数
JSON_HELP(sex,age,name)
};
// JSON_HELP 是一个变参宏
#define JSON_HELP(...) \
UNMARSHAL_OBJ(__VA_ARGS__) \ //这里生成unmarshal函数
MARSHAL_OBJ(__VA_ARGS__)
/*
* UNMARSHAL_OBJ中FOR_EACH宏第一个参数传入一个函数,第二个参数传入一个list
* 作用是对list中每个元素调用传入的函数,这里有点像python里高阶函数map()的味道
* 这样就批量生成了下面的代码结构:
* if(!Json::Unmarshal(field_1,root["field_1"])){
* ret = false;
* }
* if(!Json::Unmarshal(field_2,root["field_2"])){
* ret = false;
* }
* ... ..
*/
#define UNMARSHAL_OBJ(...) \
bool unmarshal(const Json::Value& root){ \
bool ret = true; \
FOR_EACH(__unmarshal_obj_each_field__,__VA_ARGS__) \
return ret; \
}
#define __unmarshal_obj_each_field__(field) \
if(!Json::Unmarshal(field,root[#field])){ \
ret = false; \
}
//###### FOR_EACH 实现#######
//作用:传入一个函数func和一个list,把func用在list的每个元素上
#define FOR_EACH(func,...) \
MACRO_CAT(__func_,COUNT(__VA_ARGS__))(func,__VA_ARGS__)
/*
* FOR_EACH在实现中 COUNT宏用于统计参数个数,返回一个数字,MACRO_CAT宏用于把两个token连接起来,
* 如果__VA_ARGS__有三个变量为a,b,c 那么这一步宏展开后为:
* __func_3(func,a,b,c), 而__func_3 __func_2 __func_1 ... 定义如下
* /
// 宏展开实现伪循环
/*
* __func_3(func,a,b,c) 具体展开过程:
* 第一次: __func_1(func,a) __func_2(func,b,c)
* 第二次: func(a) __func_1(func,b) __func_1(func,c)
* 第三次: func(a) func(b) func(c)
* 最终在a,b,c上都调用了一次传入的func函数
*/
#define __func_1(func,member) func(member);
#define __func_2(func,member,...) __func_1(func,member) __func_1(func,__VA_ARGS__)
#define __func_3(func,member,...) __func_1(func,member) __func_2(func,__VA_ARGS__)
#define __func_4(func,member,...) __func_1(func,member) __func_3(func,__VA_ARGS__)
#define __func_5(func,member,...) __func_1(func,member) __func_4(func,__VA_ARGS__)
... ...
//###### COUNT 宏实现#######
//作用: 返回传入参数个数. eg: COUNT(a,b,c)返回3
#define COUNT(...) __count__(0, ##__VA_ARGS__, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
#define __count__(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, N, ...) N
###### MACRO_CAT 宏实现#######
//作用: 将两个token(可以是宏),连接在一起
#define MACRO_CAT(a,b) __macro_cat__(a,b)
#define __macro_cat__(a,b) a##b
测试
我们的Umarshal 和 Marshal函数编写好了,现在测试一下吧:
场景:有一个map对象存着教师的信息,每个教师又保存着ta教学生信息,数据结构定义如下:
struct Student {
long id;
bool sex;
double score;
string name;
JSON_HELP(id,sex,score,name)
};
struct Teacher {
string name;
int subject;
vector<Student> stus;
JSON_HELP(name,subject,stus)
};
map<string,Teacher> tchs; //需要序列化和反序列化的对象
测试代码:
// 对应于结构 map<string,Teacher> 的json
string ori = R"(
{
"Tea_1": {
"name": "Tea_1",
"subject": 3,
"stus": [
{
"id": 201721020126,
"sex": false,
"score": 80,
"name": "Stu.a"
},
{
"id": 201101101537,
"sex": true,
"score": 0,
"name": "Stu.b"
}
]
},
"Tea_2": {
"name": "Tea_2",
"subject": 1,
"stus": [
{
"id": 201521020128,
"sex": true,
"score": 59,
"name": "Stu.c"
}
]
}
}
)";
int main() {
map<string,Teacher> tchs;
// 从json字符串反序列化对象
bool ret = Json::Unmarshal(tchs,ori);
if(!ret){
cout<<"反序列失败"<<endl;
return 0;
}else{
cout<<"反序列成功"<<endl;
}
// 序列化对象到 json字符串
cout<<"输出对象序列化的json:"<<endl;
string obj2json;
Json::Marshal(tchs,obj2json);
cout<<obj2json;
}
//##### 输出结果#####
反序列成功
输出对象序列化的json:
{"Tea_1":{"name":"Tea_1","stus":[{"id":201721020126,"name":"Stu.a","score":80.0,"sex":false},{"id":201101101537,"name":"Stu.b","score":0.0,"sex":true}],"subject":3},"Tea_2":{"name":"Tea_2","stus":[{"id":201521020128,"name":"Stu.c","score":59.0,"sex":true}],"subject":1}}
完整例子地址:https://git.xiaojukeji.com/sunriseyangxin/easyjson.git
作者:杨昕
来源:oschina
链接:https://my.oschina.net/u/4244677/blog/4469416