Closed DistinctWind closed 1 year ago
@3069795515 阅读Boost.Json官方文档和其他资料,考察其易用性。
简要阅读Boost.Json库的官方文档后,提取到以下信息:
boost::json::value
JSON对象有若干种类型可以选择,一个字符串、一个布尔值、一个字典、一个数组都可以是一个JSON格式的对象。boost::json::value
类提供了一个统一的容器,可以在这个容器中装下以上类型。
该容器有一定的自适应性,但想要精确定义数据结构,就不能依赖自动推断。
示例:
#include <boost/json.hpp>
#include <iostream>
int main(int argc, char* argv[]) {
boost::json::value val = {
"This",
"is",
"a",
"array"};
std::cout << val << std::endl;
return 0;
}
输出:
["This","is","a","array"]
注意,该JSON库中大部分类都重载了operator<<
,能直接使用流输出进行序列化。
boost::json::array
根据官方文档,该类在“接口设计上”有意向std::vector
看齐,作为JSON对象的数组,支持动态插入值(使用emplace
或emplace_back
)
直接把上文的例子改成使用boost::json::array
类型,得到的效果是一样的。
#include <boost/json.hpp>
#include <iostream>
int main(int argc, char* argv[]) {
boost::json::array val = {
"This",
"is",
"a",
"array"};
std::cout << val << std::endl;
return 0;
}
boost::json::object
表示了一个JSON对象,主要指的是JSON的字典格式,以键值对的形式定义。
在初始化的过程中,接受若干个键值对,表现为双层的initializer_list
。外层为键值对列表,内层为含有两个元素的initializer_list
,左边为键,右边为值。
#include <boost/json.hpp>
#include <iostream>
int main(int argc, char* argv[]) {
boost::json::object val {
{"name", "John"},
{"age", 18}
};
std::cout << val << std::endl;
return 0;
}
输出:
{"name":"John","age":18}
注意,键必须为字符串,但是值可以为任意合理的JSON值类型,比如布尔值、数字等。
boost::json::string
是一个字符串类型,Boost.Json库假定该字符串为UTF-8格式。但这个类无关紧要,我们可以使用std::string
代替他。
重点应该是命名冲突的问题,这提醒我们不能直接无脑using namespace std;
。
可以在函数体的范围内适当地使用using
声明,但使用using
指示时要额外小心。
比如说,在一个JSON对象中,需要最外层是一个字典,内层的值包含了一个列表。
此时boost::json::value
的自动推导能起作用。比如我们弄一个student
的JSON结构体:
#include <boost/json.hpp>
#include <iostream>
int main(int argc, char* argv[]) {
boost::json::value student{
{"name", "John"},
{"age", 18},
{"scores", {99, 100, 87}}};
std::cout << student << std::endl;
return 0;
}
输出:
{"name":"John","age":18,"scores":[99,100,87]}
不依赖自动推导,则应该这样指明类型:
#include <boost/json.hpp>
#include <iostream>
int main(int argc, char* argv[]) {
boost::json::value student = boost::json::object{
{"name", "John"},
{"age", 18},
{"scores", boost::json::array{99, 100, 87}}};
std::cout << student << std::endl;
return 0;
}
得到同样的结果。
注意,虽然结果相同,但是知晓其底层类型依然是重要的,因为在实际序列化时没有办法像这样将想要序列化的值硬编码到程序中,我们只能对JSON对象的具体类型进行操作。
这个可能比较有用,使用token流的例子,我们让一个JSON字典表示一个token,那么token流就是一个JSON数组了,这个数组里的每一个元素都是token字典。
同样可以自动推导:
#include <boost/json.hpp>
#include <iostream>
int main(int argc, char* argv[]) {
boost::json::value tokens = {
{
{"type", "INT"},
{"line", 10}
},
{
{"type", "CLASS"},
{"line", 20}
}
};
std::cout << tokens << std::endl;
return 0;
}
输出:
[{"type":"INT","line":10},{"type":"CLASS","line":20}]
或者使用完整类型:
#include <boost/json.hpp>
#include <iostream>
int main(int argc, char* argv[]) {
boost::json::value tokens = boost::json::array{
boost::json::object{
{"type", "INT"},
{"line", 10}
},
boost::json::object{
{"type", "CLASS"},
{"line", 20}
}
};
std::cout << tokens << std::endl;
return 0;
}
得到同样的结果。
boost::json::object
该类被设计为像std::map
一样,可以直接给键值对赋值:
#include <boost/json.hpp>
#include <iostream>
int main(int argc, char* argv[]) {
boost::json::object obj;
obj["name"] = "john";
obj["age"] = 18;
std::cout << obj << std::endl;
return 0;
}
{"name":"john","age":18}
boost::json::array
该类被设计为像std::vector
一样,通常使用push_back
或者emplace_back
添加元素。
注意,emplace_back
接受的构造参数会被传到boost::json::value
当中,此处使用临时量(右值)移动的方式演示emplace_back
。
#include <boost/json.hpp>
#include <iostream>
int main(int argc, char* argv[]) {
boost::json::array arr;
arr.push_back(10);
arr.emplace_back(
boost::json::value{
{"key", "value"},
});
std::cout << arr << std::endl;
return 0;
}
输出:
[10,{"key":"value"}]
基于以上知识,可以初步设计一个序列化API。
serializable
接口类为纯虚基类serializable
,定义如下:
class serializable {
virtual boost::json::value to_json() = 0;
};
实现了该接口的函数负责依据自身信息构造一个json对象。
比如token类应该这样序列化自身:
struct token: serializable {
enum class type {
INT,
CLASS
};
inline static const std::map<type, std::string> type_name{
{type::INT, "INT"},
{type::CLASS, "CLASS"},
};
type _type;
int line;
token(type _type, int line):
_type{_type}, line{line} {
}
boost::json::value to_json() override {
using namespace boost;
json::value result = {
{"type", type_name.at(_type)},
{"line", line}};
return result;
}
};
另一方面,对于组合了多个token的token流类(token_stream
),则其序列化结果应为一个JSON数组,并且依赖于每一个token进行JSON序列化的结果。
struct token_stream: serializable {
std::vector<token> tokens;
token_stream(std::initializer_list<token> tokens):
tokens{tokens} {
}
boost::json::value to_json() override {
using namespace boost;
json::array result;
for (auto&& tok: tokens) {
auto serialized_tok = tok.to_json();
result.emplace_back(serialized_tok);
}
return result;
}
};
此处使用emplace_back
插入JSON对象。
最后,调用该API:
int main() {
using namespace std;
using namespace boost;
token_stream tokenStream{
{token::type::INT, 10},
{token::type::CLASS, 20},
};
cout << tokenStream.to_json();
return 0;
}
得到预期的结果:
[{"type":"INT","line":10},{"type":"CLASS","line":20}]
补充反序列化
通过boost::json::parse
函数,接受一个字符串,得到boost::json::value
类型的对象。
#include <boost/json.hpp>
#include <iostream>
int main(int argc, char* argv[]) {
boost::json::value result = boost::json::parse(
R"([1,2,3,{"key": "value"}])"
);
std::cout << result << std::endl;
return 0;
}
输出:
[1,2,3,{"key":"value"}]
(这个示例很明显是反序列化后再序列化……仅做演示用途)
补充配置说明
Boost.Json库是Boost库中少数非Header Only的库之一。需要链接对应的库文件才能使用。幸运的是,我们已经使用VCPKG作为包管理工具,编译安装Boost库并不需要我们操心,我们只需要考虑如何链接到Boost.Json就可以了。
在cmake文件中,让FindBoost
模块搜寻组件json
:
find_package(Boost REQUIRED
COMPONENTS json
)
然后,Boost.Json对应的库文件就会导出到变量Boost_LIBRARIES
中。
接下来,给构建目标配置加上头文件依赖和链接依赖:
add_executable(boost_json_learning main.cpp)
target_include_directories(boost_json_learning PRIVATE ${Boost_INCLUDE_DIRS})
target_link_libraries(boost_json_learning PRIVATE ${Boost_LIBRARIES})
正如Catch2的使用方法一样。此时目标boost_json_learning
即可使用Boost.Json库并成功编译。
经以上考察,Boost.Json库的易用性和通用性足够,可以进入项目以来当中。
Boost.Json的学习已经完成
我们需要一种序列化手段,将编译器获得的token、抽象语法树等信息序列化。而目前最常用的的序列化手段就是JSON了。Boost库又是C++的准标准库,相信这个库会比较合适。