背景
JSON
库和反射库算是C++
的日经话题了,尤其是编译期反射,通常都是模板重灾区,很多很多人都想搞出一个简单快速的JSON解析库和反射库(尤其是编译期反射),以及序列化库。
当然我肯定也不例外,最近我也思考了一下如何构建一个以方便好用为最终目的的JSON
库,最好方便的跟python
一样,同时我还希望能够很方便未来扩展更多的类性支持,至于性能麻,暂时就不在考虑范围内了。
至于起因动机,一部分原因是因为我现在工作的公司选择了rapidJSON
,通常写出来都是这个码风:
int main() {
// 创建一个 Document 对象
Document doc;
// 设置为对象类型
doc.SetObject();
// 获取分配器
Document::AllocatorType& allocator = doc.GetAllocator();
// 添加 name 属性
doc.AddMember("name", Value("Bob", allocator), allocator);
// 添加 age 属性
doc.AddMember("age", Value(30), allocator);
// 添加 friends 属性
Value friends(kArrayType);
// 创建第一个 friend 对象
Value friend1(kObjectType);
friend1.AddMember("name", Value("Alice", allocator), allocator);
friend1.AddMember("age", Value(25), allocator);
// 将第一个 friend 对象添加到 friends 数组中
friends.PushBack(friend1, allocator);
// 创建第二个 friend 对象
Value friend2(kObjectType);
friend2.AddMember("name", Value("Charlie", allocator), allocator);
friend2.AddMember("age", Value(28), allocator);
// 将第二个 friend 对象添加到 friends 数组中
friends.PushBack(friend2, allocator);
// 将 friends 数组添加到 doc 对象中
doc.AddMember("friends", friends, allocator);
// 添加 scores 属性
Value scores(kObjectType);
scores.AddMember("math", Value(90), allocator);
scores.AddMember("english", Value(80), allocator);
scores.AddMember("physics", Value(85), allocator);
// 将 scores 对象添加到 doc 对象中
doc.AddMember("scores", scores, allocator);
// 创建一个 StringBuffer 对象
StringBuffer buffer;
// 创建一个 Writer 对象
Writer<StringBuffer> writer(buffer);
// 将 doc 对象写入 buffer 中
doc.Accept(writer);
// 输出 buffer 中的内容
std::cout << buffer.GetString() << std::endl;
return 0;
}
有没有办法简化代码编写,例如像写python
的dict
那样去写出一个JSON
?
import json
doc={
"name":"Bob",
"age":30,
"friends":[
{
"name":"Alice",
"age":25
},
{
"name":"Charlie",
"age":28
}
],
"scores":{
"math":90,
"english":80,
"physics":85
}
}
json_str=json.dumps(doc,indent=4)
print(json_str)
设计一个以易用为目标的JSON库
基于这个想法,我折腾出了一个以使用简单为目的的JSON
库,由于完全没有评估和优化过性能,可能会极其拉垮,故取名为SlowJSON
。
SlowJSON
提供大量对于STL
容器和自定义类的JSON
序列化与反序列化的支持,并以pythonic
的语法为接口设计目标。
SlowJSON
可以以这样的方法去构建一个JSON
:
#include "slowjson.hpp"
#include "iostream"
int main() {
slow_json::static_dict doc{
std::pair{"name", "Bob"},
std::pair{"age", 30},
std::pair{"friends",std::tuple{
slow_json::static_dict{
std::pair{"name", "Alice"},
std::pair{"age", 25}
},
slow_json::static_dict{
std::pair{"name", "Charlie"},
std::pair{"age", 28}
}
}},
std::pair{"scores", slow_json::static_dict{
std::pair{"math", 90},
std::pair{"english", 80},
std::pair{"physics", 85}
}}
};
slow_json::Buffer buffer{100};
slow_json::dumps(buffer,doc,4);
std::cout<<buffer<<std::endl;
}
当然不仅于此,实际上他可以直接支持STL
容器和容器适配器,也包括std::tuple
,std::optional
,T*
,T[N]
,std::string
,std::shared_ptr
等常见C++
类型,并可以将nullptr
和std::nullopt
处理为null
。
#include "slowjson.hpp"
#include "iostream"
int main() {
slow_json::static_dict doc{
std::pair{"map",std::unordered_map<std::string,std::string>{{"key","value"}}},
std::pair{"empty",nullptr},
std::pair{"vector",std::vector{std::pair{1,2},std::pair{3,4}}}
};
slow_json::Buffer buffer{100};
slow_json::dumps(buffer,doc,4);
std::cout<<buffer<<std::endl;
}
运行这段代码,将得到如下的结果
{
"map":{
"key":"value"
},
"empty":null,
"vector":[
[
1,
2
],
[
3,
4
]
]
}
值得一提的是,SlowJSON是支持枚举型变量的,可以将字符串解析为枚举型变量,或将枚举型变量解析为字符串,同样不需要手工编写相关代码。
enum Color {
RED,
GREEN,
BLUE,
BLACK
};
int main() {
slow_json::Buffer buffer;
slow_json::dumps(buffer, RED, 4); //enum转化为字符串
std::cout << buffer << std::endl;
Color color2;
slow_json::loads(color2, "\"BLUE\""); //注意,这里是个字符串,带有双引号的
std::cout << (color2 == BLUE);
}
运行代码输出结果如下:
RED
1
静态JSON访问
static_dict
顾名思义,是一个静态字典,所谓静态,意思就是字段名是编译期确定的,而访问元素也是编译期确定的,实际上我没太考虑访问数据的问题,因此static_dict
提供了一个很弱的编译期访问和修改数据的接口(其实只需要要求key
是编译期变量就行,value
是不要求的,这里只是举例说明static_dict
可以编译期构造)
using namespace slow_json::static_string_literals; //为了支持_ss后缀,用来获取编译期静态字符串
int main(){
slow_json::Buffer buffer(1000);
constexpr slow_json::static_dict dict{
std::pair{"test"_ss, 123},
std::pair{"name"_ss, "ABC"},
std::pair{"tuple"_ss, slow_json::static_dict{
std::pair{"haha"_ss, "wawa"},
std::pair{"single"_ss, "boy"}
}}
};
constexpr auto value=dict["name"_ss];
constexpr auto value2=dict["tuple"_ss]["haha"_ss];
std::cout<<value<<" "<<value2<<std::endl;
代码输出结果为:
ABC wawa
这里的_ss
实际上是slow_json::StaticString<chs...> operator ""_ss()
,获取一个编译期静态字符串,直接使用字符换字面量是无法实现编译期访问的
如果编译期无法找到这个key
,你会得到一个编译错误:
./slowjson/static_dict.hpp:29:41: error: static assertion failed: 找不到对应的元素
对于用户自定义类的支持(侵入式)
对于用户自定义的类,也能够很好的提供支持,只需要添加一个get_config函数即可,多个类可以互相嵌套混合(对于派生类也是支持的,详细可以见github
的readme.md
)
using namespace slow_json::static_string_literals;
struct Node {
int x = 1;
std::vector<float> y = {1.2, 3.4};
std::string z = "STR";
static constexpr auto get_config() noexcept {
return slow_json::static_dict{
std::pair{"x"_ss, &Node::x},
std::pair{"y"_ss, &Node::y},
std::pair{"z"_ss, &Node::z}
};
}
};
struct NodeList {
Node nodes[3]; //嵌套类,两个自定义类都是可序列化的,组合起来也是可序列化的
static constexpr auto get_config() noexcept {
return slow_json::static_dict{
std::pair{"nodes"_ss, &NodeList::nodes}
};
}
};
int main() {
slow_json::Buffer buffer(1000);
NodeList node_list;
node_list.nodes[2].z="change";
//序列化node_list
slow_json::dumps(buffer, node_list);
//反序列化到node_list2上去
NodeList node_list2;
slow_json::loads(node_list2,buffer.string());
buffer.clear();
//然后再次序列化node_list2,查看结果是否正确
slow_json::dumps(buffer,node_list2,4);
std::cout<<buffer<<std::endl;
}
运行这段代码将会得到如下的输出
{
"nodes":[
{
"x":1,
"y":[
1.2,
3.4,
1.2,
3.4
],
"z":"STR"
},
{
"x":1,
"y":[
1.2,
3.4,
1.2,
3.4
],
"z":"STR"
},
{
"x":1,
"y":[
1.2,
3.4,
1.2,
3.4
],
"z":"change"
}
]
}
对于用户自定义类的支持(非侵入式)
除了上述的侵入式接口,非侵入式接口也是支持的,毕竟很多类型我们没法去改代码,例如OpenCV
的点和矩阵。
这里提一嘴就是,SlowJSON
是采用模板特化和类型匹配来实现的,因此想要继续拓展新的类型,只需要提供对应的特化类实现即可。
例如如果希望提供对于cv::Mat
的支持,可以编写如下的代码
namespace slow_json {
// 提供序列化的支持
template<>
struct DumpToString<cv::Mat> : public IDumpToString<DumpToString<cv::Mat>> {
static void dump_impl(Buffer &buffer, const cv::Mat &value) noexcept {
std::vector<std::vector<int>> vec; //将cv::Mat转化为已知的可以处理的类型,然后调用对应类型的特化类的静态方法即可
for (int i = 0; i < value.cols; i++) {
std::vector<int> line;
for (int j = 0; j < value.rows; j++) {
line.emplace_back(value.at<int>(i, j));
}
vec.emplace_back(std::move(line));
}
// slow_json::dumps(buffer,vec); //直接这样写也行,不过我感觉最好明确类型,不然代码逻辑可能会很混乱
DumpToString<decltype(vec)>::dump(buffer, vec);
}
};
// 提供反序列化的实现
template<>
struct LoadFromDict<cv::Mat> : public ILoadFromDict<LoadFromDict<cv::Mat>> {
static void load_impl(cv::Mat &value, const slow_json::dynamic_dict &dict) {
value = cv::Mat(3, 3, CV_8UC1);
for (int i = 0; i < dict.size(); i++) {
for (int j = 0; j < dict[i].size(); j++) {
value.at<uint8_t>(i, j) = dict[i][j].cast<int32_t>();//后面会介绍slow_json::dynamic_dict
}
}
}
};
}
然后就可以继续快乐的得到JSON和把JSON还原为对应的对象了
struct ImageMerger {
int x = 100, y = 120, w = 1000, h = 2000;
cv::Mat transform_mat = (cv::Mat_<int>(3, 3) << 1, 2, 3, 4, 5, 6, 7, 8, 9);
static constexpr auto get_config() noexcept {
return slow_json::static_dict{
std::pair{"x"_ss, &ImageMerger::x},
std::pair{"y"_ss, &ImageMerger::y},
std::pair{"w"_ss, &ImageMerger::w},
std::pair{"h"_ss, &ImageMerger::h},
std::pair{"transform_mat"_ss, &ImageMerger::transform_mat}
};
}
};
int main() {
slow_json::Buffer buffer(1000);
ImageMerger merger;
slow_json::dumps(buffer, merger);
std::cout << buffer << std::endl;
}
运行代码得到结果如下:
{"x":100,"y":120,"w":1000,"h":2000,"transform_mat":[[1,2,3],[4,5,6],[7,8,9]]}
动态JSON解析
很多时候,可能我们并不是真的想把JSON
处理为C++
对象,我们希望直接处理JSON
子段,或者像上述非侵入式写法中需要手工去处理JSON
。
因此SlowJSON
也提供了和python
的dict
十分相似的接口设计,用户可以像python
的字典那样去访问数据,而不必非得写一个对应的class
。
int main() {
std::string json_str = R"({
"x":[4],
"y":[1],
"z":[2,3,4,5,6],
"t":null,
"object":{
"name":"zhihu"
}
})";
slow_json::dynamic_dict dict(json_str);
std::cout << dict["object"]["name"].cast<std::string>() << std::endl; //直接访问数据
std::cout << dict["t"].empty() << std::endl; //是否为null
std::cout << dict["z"].size() << std::endl; //获取数组大小(如果是一个数组的话)
for(int i=0;i<dict["z"].size();i++){
std::cout<<dict["z"][i].cast<int>()<<" ";
}
std::cout<<std::endl;
auto z=dict["z"].cast<std::vector<int>>(); //将其直接解析为std::vector<int>,这里实际是在进行反序列化
for(auto&it:z){
std::cout<<it<<" ";
}
std::cout<<std::endl;
}
运行得到的结果如下:
zhihu
1
5
2 3 4 5 6
2 3 4 5 6
基于多态的JSON字典类型(slow_json::polymorphic_dict)
slow_json::static_dict<Args...>
是一个模板类型,无法事先确定类型,get_config
的返回值类型只能写为auto
,这对于一些喜欢头文件和实现分离的人来说,并不是很友好。
因此SlowJSON
中还提供了slow_json::polymorphic_dict
,该类型不是一个模板类型,可以将get_config
写为 slow_json::polymorphic_dict Node::get_config() noexcept
。
当然这样会牺牲一些性能(顾名思义,有额外虚函数开销),并且无法无法访问具体数据,只能服务于从对象到JSON
或从JSON
到对象的过程。其用于get_config
中的时候,同样可以让Node
同时支持序列化与反序列化的。另外,slow_json::dumps
也支持将其转化为JSON
字符串。
struct Node {
int xxx = 1;
float yyy = 1.2345;
std::string zzz = "shijunfeng";
std::deque<std::string> dq{"a", "b", "c", "d"};
static slow_json::polymorphic_dict get_config() noexcept;
};
slow_json::polymorphic_dict Node::get_config() noexcept {
return slow_json::polymorphic_dict{
std::pair{"xxx"_ss, &Node::xxx},
std::pair{"yyy"_ss, &Node::yyy},
std::pair{"zzz"_ss, &Node::zzz},
std::pair{"dq"_ss, &Node::dq}
};
}
int main() {
Node p;
slow_json::Buffer buffer;
slow_json::dumps(buffer, p, 4);
std::cout << buffer << std::endl;
}
对于不支持的类型的处理
框架除了要使用简单,我们同行还希望报错简单。是模板编程很容易搞出一大串错误,很难找有用信息,很难查找问题。
在SlowJSON
中,由于采用了特化,因此在找不到对应特化实现的时候,总是可以去找非特化的默认函数,我们可以利用这一点并结合CRTP
获得子类信息来生成一个运行时错误。
对于不支持的类型,大多数情况下你会在debug
模式得到一个运行时的断言失败,例如:
程序断言失败,程序被迫结束
断言表达式:(SLOW_JSON_SUPPORTED)=false
文件:/project/石峻峰-实验性项目/SlowJson重构/slowjson/dump_to_string_interface.hpp
行数:30
函数完整签名:static void slow_json::DumpToString<T>::dump_impl(slow_json::Buffer&, const T&) [with T = cv::Mat]
断言错误消息:无法将类型为'cv::Mat'的对象正确转换为字符串,找不到对应的DumpToString特化类
terminate called without an active exception
查寻类型是否被支持
其实SlowJSON也提供了接口来查寻某个类型是否被支持的,但是好像用处不是很大。
using namespace slow_json::static_string_literals;
struct Test{
int a,b;
constexpr static auto get_config()noexcept{
return slow_json::static_dict{
std::pair{"a"_ss,&Test::a},
std::pair{"b"_ss,&Test::b}
};
}
};
int main(){
static_assert(slow_json::concepts::dump_supported<Test>); //是否支持序列化
static_assert(slow_json::concepts::load_supported<Test>); //是否支持反序列化
static_assert(slow_json::concepts::supported<Test>); //是头同时支持序列化和反序列化
}
结束语
-
以上只是一个简要说明,详细的请参考github的readme
-
目前为止还只是一个粗糙的版本,可能有很多潜在的BUG,欢迎大家批评指正
-
如果觉得有帮助的话,快去github上帮忙点颗star吧!