如何构建一个以使用简单为核心目标的轻量级C++ JSON库

github项目链接在这里

背景

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;
}

有没有办法简化代码编写,例如像写pythondict那样去写出一个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::tuplestd::optional,T*,T[N],std::string,std::shared_ptr等常见C++类型,并可以将nullptrstd::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函数即可,多个类可以互相嵌套混合(对于派生类也是支持的,详细可以见githubreadme.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也提供了和pythondict十分相似的接口设计,用户可以像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吧!

  • 7
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值