用于在C++中表示 JSON 的数据类型
Datatypes for representing JSON in C++
我一直在试图弄清楚这个问题,也许我只是盯着它看了太久?
无论如何,手头的问题是找到一种在 C++ 中表示 JSON 的好方法,在您阅读之前,请注意我对能够使用它的库不感兴趣,所以我想用原始 C 或 C++ 来做(C++11 很好),没有提升,没有 libjson 我知道它们,并且由于此问题范围之外的原因,我不能(/wont)添加依赖项。
现在这个问题已经解决,让我告诉你一些关于这个问题的信息,以及我到目前为止所尝试的。
问题是找到一种在C++中表示 JSON 的好方法,这有点问题的原因是 JSON 是超松散类型的,而 C++JSON 实际上是硬类型的。 考虑一下JSON,JSON在类型上真正能够做什么?
- 数字(例如
42
或3.1415
) - 字符串(例如
"my string"
) - 阵列(例如
[]
或[1,3.1415,"my string]
) - 对象(例如
{}
或{42, 3.1415, "my string", [], [1,3.1415, "my string]}
所以这意味着有两种"原始"类型,数字和字符串,以及两种容器类型数组和对象。原始类型相当简单,而容器类型在 C/C++ 中变得棘手,因为它们可以并且可能会包含不同类型的元素,因此语言中的任何内置类型都不够,数组不能容纳不同类型的元素。这也适用于 STL 类型(列表、向量、数组等)(除非它们具有多态相等性)。
因此,JSON 中的任何容器都可以容纳任何类型的 json 类型,这几乎就是它的全部内容。
我已经制作了什么原型,或者尝试了什么,为什么它不起作用 我的第一个天真的想法是只使用模板,所以我设置了一个 json-object 或 json-node 类型,然后使用模板来决定其中的内容,所以它会有一个这样的结构:
template <class T>
class JSONNode {
const char *key;
T value;
}
虽然这看起来很有希望,但是当开始使用它时,我意识到当我尝试将节点排序为容器类型(例如数组、向量、unordered_map等)时,我遇到了麻烦,因为他们仍然想知道该 JSONNode 的类型! 如果一个节点被定义为JSONNode<int>
而另一个节点JSONNode<float>
得很好,那么将它们放在容器中将是有问题的。
所以我超越了这一点,无论如何,我对将它们保存在容器中并不那么感兴趣,我很乐意让它们具有自我意识或称呼它,即在指向下一个节点的指针中 ad,但再次弄清楚节点的类型变得棘手,几乎是我开始思考多态性的时候。
多态性让我们创建一个虚拟JSONNode
并实现一个JSONNumberNode, JSONStringNode, JSONArrayNode
和JSONObjectNode
类型,它们将很好地适合我可能想要它们的任何容器,使用多态性让它们都是 JSONNodes。
代码的示例可能已到位。
class JSONNode {
public:
const char *key;
//?? typed value, can't set a type
};
class JSONNumberNode : public JSONNode {
public:
int value;
}
class JSONStringNode : public JSONNode {
public:
const char *value;
}
起初我以为这是要走的路。然而,当我开始考虑如何处理值部分时,我意识到我无法访问值,即使我编写了一个特定的函数来检索值,它会返回什么?
所以可以肯定的是,我确实有具有不同类型化值的对象,但是如果不首先转换为正确的类型,我就无法真正访问它们,所以我可以做一个dynamic_cast<JSONStringNode>(some_node);
,但是我怎么知道要将其转换为什么?RTTI?好吧,我觉得在这一点上它变得有点复杂,我想我可以使用一种类型或 decltype 来弄清楚将其类型转换为什么,但还没有成功。
容器类型所以我尝试了一些不同的东西,我想争辩说也许我实际上可以用豆荚的方式做到这一点。然后,我将value
部分设置为void *
,并尝试使用一些union
来跟踪类型。但是,我遇到了与我已经遇到的相同的问题,即如何将数据转换为类型。
我觉得有必要包装这个问题,为什么我没有更深入地了解我尝试使用 POD 的内容。
因此,如果有人有一个聪明的解决方案来表示 JSON C++给定这些信息,我将不胜感激。
我认为你上一种方法的方向是正确的,但我认为它需要改变一些概念分配。
到目前为止,在我工作过的所有 JSON 解析器中,选择容器类型的决定是在用户端而不是在解析器端,我认为这是一个明智的决定,为什么? 假设您有一个节点,其中包含一个字符串格式的数字:
{
"mambo_number": "5"
}
您不知道用户是否希望以字符串或数字的形式检索值。因此,我要指出的是,JSONNumberNode
和JSONStringNode
不适合最佳方法。我的建议是创建用于保存对象、数组和原始值的节点。
所有这些节点都将包含一个标签(名称)和一个嵌套对象列表(根据其主要类型):
-
JSONNode
:包含节点键和节点类型的基节点类。 -
JSONValueNode
:管理和包含原始值的节点类型,如上面列出的曼波nº5,它将提供一些函数来读取其值,如value_as_string()
、value_as_int()
、value_as_long()
,到目前为止... -
JSONArrayNode
:管理 JSON 数组并包含按索引访问JSONNode
节点类型。 -
JSONObjectNode
:管理 JSON 对象并包含可按名称访问JSONNode
节点类型。
我不知道这个想法是否有据可查,让我们看看一些例子:
例 1
{
"name": "murray",
"birthYear": 1980
}
上面的 JSON 将是一个未命名的根JSONObjectNode
,其中包含两个带有标签 name
和 birthYear
的JSONValueNode
。
例 2
{
"name": "murray",
"birthYear": 1980,
"fibonacci": [1, 1, 2, 3, 5, 8, 13, 21]
}
上面的 JSON 将是一个未命名的根JSONObjectNode
,其中包含两个 JSONValueNode
和一个 JSONArrayNode
。JSONArrayNode
将包含 8 个未命名的JSONObjectNode
,其中包含斐波那契数列的 8 个前值。
例 3
{
"person": { "name": "Fibonacci", "sex": "male" },
"fibonacci": [1, 1, 2, 3, 5, 8, 13, 21]
}
上面的 JSON 将是一个未命名的根JSONObjectNode
,其中包含一个带有两个JSONValueNode
的JSONObjectNode
,标签为 name
和 sex
以及一个JSONArrayNode
。
例 4
{
"random_stuff": [ { "name": "Fibonacci", "sex": "male" }, "random", 9],
"fibonacci": [1, 1, 2, 3, 5, 8, 13, 21]
}
上面的 JSON 将是一个包含两个JSONArrayNode
的未命名根JSONObjectNode
,第一个标记为 random_stuff
将包含 3 个未命名JSONValueNode
其类型为 JSONObjectNode
、JSONValueNode
和 JSONValueNode
按出现顺序排列,第二个JSONArrayNode
是之前注释的斐波那契数列。
实现
我将面对节点实现的方式如下:
基节点将通过成员type
知道它自己的类型(值节点、数组节点或对象节点),type
的值由派生类在构造时提供。
enum class node_type : char {
value,
array,
object
}
class JSONNode {
public:
JSONNode(const std::string &k, node_type t) : node_type(t) {}
node_type GetType() { ... }
// ... more functions, like GetKey()
private:
std::string key;
const node_type type;
};
派生类必须在构造时向基元提供节点的类型,值节点向用户提供存储值到用户请求的类型的转换:
class JSONValueNode : JSONNode {
public:
JSONValueNode(const std::string &k, const std::string &v) :
JSONNode(k, node_type::value) {} // <--- notice the node_type::value
std::string as_string() { ... }
int as_int() { ... }
// ... more functions
private:
std::string value;
}
数组节点必须提供operator[]
才能将其用作数组;实现一些迭代器是值得的。内部std::vector
的存储值(选择您认为最适合此目的的容器)将JSONNode
的。
class JSONArrayNode : JSONNode {
public:
JSONArrayNode(const std::string &k, const std::string &v) :
JSONNode(k, node_type::array) {} // <--- notice the node_type::array
const JSONObjectNode &operator[](int index) { ... }
// ... more functions
private:
std::vector<JSONNode> values;
}
我认为对象节点必须为operator[]
提供字符串输入,因为C++我们无法复制 JSON node.field
访问器,实现一些迭代器将是值得的。
class JSONObjectNode : JSONNode {
public:
JSONObjectNode(const std::string &k, const std::string &v) :
JSONNode(k, node_type::object) {} // <--- notice the node_type::object
const JSONObjectNode &operator[](const std::string &key) { ... }
// ... more functions
private:
std::vector<JSONNode> values;
}
用法
假设所有节点都具有所有必需的功能,则使用我的aproach的想法将是:
JSONNode root = parse_json(file);
for (auto &node : root)
{
std::cout << "Processing node type " << node.GetType()
<< " named " << node.GetKey() << 'n';
switch (node.GetType())
{
case node_type::value:
// knowing the derived type we can call static_cast
// instead of dynamic_cast...
JSONValueNode &v = static_cast<JSONValueNode>(node);
// read values, do stuff with values
break;
case node_type::array:
JSONArrayNode &a = static_cast<JSONArrayNode>(node);
// iterate through all the nodes on the array
// check what type are each one and read its values
// or iterate them (if they're arrays or objects)
auto t = a[100].GetType();
break;
case node_type::object:
JSONArrayNode &o = static_cast<JSONObjectNode>(node);
// iterate through all the nodes on the object
// or get them by it's name check what type are
// each one and read its values or iterate them.
auto t = o["foo"].GetType();
break;
}
}
笔记
我不会使用Json-Whatever-Node
命名约定,我倾向于将所有东西放入命名空间并使用较短的名称;在命名空间的范围之外,该名称非常可读且不可更改:
namespace MyJSON {
class Node;
class Value : Node;
class Array : Node;
class Object : Node;
Object o; // Quite easy, short and straightforward.
}
MyJSON::Node n; // Quite readable, isn't it?
MyJSON::Value v;
我认为值得为每个对象创建空版本,以便在无效访问的情况下提供:
// instances of null objects
static const MyJSON::Value null_value( ... );
static const MyJSON::Array null_array( ... );
static const MyJSON::Object null_object( ... );
if (rootNode["nonexistent object"] == null_object)
{
// do something
}
前提是:返回 null 对象类型,即访问对象节点中不存在的子对象或越界访问数组节点的情况。
希望对您有所帮助。
您的最后两个解决方案都有效。您在两者中的问题似乎是提取实际值,所以让我们看一下示例。我将介绍 POD 的想法,原因很简单,使用多态确实需要 RTTI,恕我直言,这很丑陋。
杰森:
{
"foo":5
}
你加载这个JSON文件,你会得到的只是你的POD"包装器"。
json_wrapper wrapper = load_file("example.json");
现在,假设加载的 JSON 节点是 JSON 对象。您现在必须处理两种情况:要么它是对象,要么不是。如果不是,则最终可能会处于错误状态,因此可以使用例外。但是,您将如何提取对象本身呢?好吧,只需使用函数调用即可。
try {
JsonObject root = wrapper.as_object();
} catch(JSONReadException e) {
std::cerr << "Something went wrong!" << std::endl;
}
现在,如果wrapper
包装的 JSON 节点确实是 JSON 对象,则可以继续在 try {
块中执行要对该对象执行的任何操作。同时,如果 JSON "格式不正确",您将进入catch() {
块。
在内部,您将实现如下:
class JsonWrapper {
enum NodeType {
Object,
Number,
...
};
NodeType type;
union {
JsonObject object;
double number
};
JsonObject as_object() {
if(type != Object) {
throw new JSONReadException;
} else {
return this->object;
}
}
我知道你说你对库不感兴趣,但我过去做过一个C++解码/编码 JSON 的库:
https://github.com/eteran/cpp-json
这是一个相当小的库,只有标题,所以你可以从中收集我的策略。
本质上,我有一个包裹boost::variant
的json::value
,所以它可以是基本类型之一(string
,number
,boolean
,null
),当然也可以是array
或object
。
前向声明和动态分配有点棘手,因为array
和object
包含value
s,而 s 又可以是 array
s 和 object
s。但这是一般的想法。
希望这有帮助。
如果你有兴趣学习,我强烈建议你通读jq源代码——它真的是干净的C代码,没有外部json库依赖。
在内部,jq 将类型信息保存在一个简单的枚举中,从而避免了大多数编译时类型问题。 尽管这确实意味着您必须建立基本操作。
我已经为JSON解析器编写了一个库。JSON 表示形式由模板类json::value
实现,符合C++标准库。它需要 C++11 和符合标准的容器。
JSON 值基于类json::variant
。这与 v1.52 boost::variant
没有什么不同,但使用的是更现代的实现(利用可变参数模板)。这种变体实现更加简洁,尽管由于无处不在的模板技术不是很简单。它只是一个文件,而boost::variant
的实现似乎过于复杂(由于设计时缺乏可变参数模板)。此外,json::variant 在可能的情况下利用移动语义,并实现一些技巧来变得相当高性能(优化的代码比 boost 1.53 中的代码快得多)。
类json::value
定义了其他一些类型,表示基元类型(数字、布尔值、字符串、Null)。对象和数组容器类型将通过模板参数定义,这些参数必须是符合标准的容器。因此,基本上可以在几个标准的lib兼容容器中进行选择。
最后,JSON 值包装了一个变体成员,并提供了一些成员函数和一个漂亮的 API,这使得使用 JSON 表示变得非常容易。
该实现具有一些不错的功能。例如,它支持"作用域分配器"。有了它,就可以在构建 JSON 表示时使用"竞技场分配器"来提高性能。这需要一个符合要求且完全实现的容器库,它支持作用域分配器模型(clang 的 std lib 就是这样做的)。但是,将此功能实现到变体类中增加了一层额外的复杂性。
另一个功能是,创建和访问表示非常容易。
下面是一个示例:
#include "json/value/value.hpp"
#include "json/generator/write_value.hpp"
#include <iostream>
#include <iterator>
int main(int argc, const char * argv[])
{
typedef json::value<> Value;
typedef typename Value::object_type Object;
typedef typename Value::array_type Array;
typedef typename Value::string_type String;
typedef typename Value::integral_number_type IntNumber;
typedef typename Value::float_number_type FloatNumber;
typedef typename Value::boolean_type Boolean;
typedef typename Value::null_type Null;
Value json = Array();
json.as<Array>().push_back("Hello JSON!");
json.as<Array>().push_back("This is a quoted "string".");
json.as<Array>().push_back("First line.nSecond line.");
json.as<Array>().push_back(false);
json.as<Array>().push_back(1);
json.as<Array>().push_back(1.0);
json.as<Array>().push_back(json::null);
json.as<Array>().push_back(
Object({{"parameters",
Object({{"key1", "value"},{"key2", 0},{"key3", 0.0}})
}}));
std::ostream_iterator<char> out_it(std::cout, nullptr);
json::write_value(json, out_it, json::writer_base::pretty_print);
std::cout << std::endl;
std::string jsonString;
json::write_value(json, std::back_inserter(jsonString));
std::cout << std::endl << jsonString << "nn" << std::endl;
}
程序将以下内容打印到控制台:
[
"Hello JSON!",
"This is a quoted "string".",
"First line.nSecond line.",
false,
1,
1.000000,
null,
{
"parameters" : {
"key1" : "value",
"key2" : 0,
"key3" : 0.000000
}
}
]
["Hello JSON!","This is a quoted "string".","First line.nSecond line.",false,1,1.000000,null,{"parameters":{"key1":"value","key2":0,"key3":0.000000}}]
当然,还有一个解析器,它可以创建这样的json::value
表示。解析器针对速度和低内存占用进行了高度优化。
虽然我认为C++表示(json::value
)的状态仍然是"Alpha",但有一个完整的Objective-C包装器,它基于C++核心实现(即解析器),可以被认为是最终的。不过,C++表示(json::value
)仍然需要一些工作才能完成。
尽管如此,该库可能是您想法的来源:代码位于 GitHub:JPJson 上,尤其是文件夹 Source/json/utility/
中variant.hpp
和mpl.hpp
的文件以及文件夹 Source/json/value/
和 Source/json/generator/
中的所有文件。
实现技术和源代码的数量可能令人费解,并且仅在iOS和Mac OS X上使用现代clang进行了测试/编译 - 请注意;)
我会实现一个只有 4 种类型的简化boost::variant
:unordered_map
、vector
、string
和(可选)数值类型(我们需要无限精度吗?
每个容器都将包含指向相同类型实例的智能指针。
boost::variant
存储它所持有类型的union
,以及它所具有的类型enum
或索引。 我们可以向它询问类型索引,我们可以询问它是否具有一个特定的类型 i,或者我们可以编写一个带有distict覆盖的访问者,variant
向其发送正确的调用。 (最后一个是apply_visitor
)。
我会模仿那个界面,因为我发现它既有用又相对完整。 简而言之,重新实现部分boost
,然后使用它。 请注意,variant
是仅标头类型,因此它可能足够轻,可以只包含。
- 防止主数据类型C++的隐式转换
- 处理小于cpu数据总线的数据类型.(c++转换为机器代码)
- 在C++中打印指向不同基元数据类型的指针的内存地址
- C++浮点数据类型和字符串数据类型无法子到模板函数中
- 如何计算数据类型的范围,例如int
- 如何使用curlpp通过POST方法上传文件和json数据
- C++中数据类型修饰符的顺序
- C++LinkedList问题.数据类型之间存在冲突?没有匹配的构造函数
- 特定数据类型的模板类
- 具有多个模板的模板函数,用于特定数据类型(如字符串)?
- 有没有办法提示用户使用哪种数据类型作为模板 c++
- int数据类型的指针指向的是什么,如果是一个类的私有数据成员,我们创建了该类的两个对象?
- 时间复杂度 当具有复合数据类型(如元组或对)时?
- 如何获取C++字符数据类型的地址
- 将复杂的非基元C++数据类型转换为 Erlang/Elixir 格式,以使用 NIF 导出方法
- 构造智能点数据类型以及普通数据类型的通用方法
- 如何使映射键具有两种不同的数据类型?
- 数据类型"struct seq<0, 1, 2>{}"含义是什么?
- 用于在C++中表示 JSON 的数据类型
- QT 哪些 QML/C++ 数据类型可以转换为 JSON