用于在C++中表示JSON的数据类型

21

我一直在尝试解决这个问题,也许我已经盯着它太久了?

无论如何,手头的问题是找到一种好的方法来表示JSON在C++中,但请注意,在您继续阅读之前,请注意,我不想使用能够处理JSON的库,所以我想用裸的C或C++(C++11可以),没有boost,没有libjson。我知道它们,但由于本问题范围之外的原因,我不能/不会添加依赖项。

既然清楚了这一点,让我告诉你一些关于问题以及我迄今为止尝试过的内容。

问题是找到一种好的方法来表示JSON在C++中,这有点棘手的原因是JSON的类型非常松散,而C++则非常强类型。考虑一下JSON,从类型上来说,JSON真正具备什么能力?

  • 数字(例如423.1415
  • 字符串(例如"my string"
  • 数组(例如[][1,3.1415,"my string]
  • 对象(例如{}{42, 3.1415, "my string", [], [1,3.1415, "my string]}

这意味着有两种“原始”类型,即数字字符串,以及两种容器类型数组对象。原始类型非常直观,而容器类型在C/C++中变得棘手,因为它们可以并且可能包含不同类型的元素,因此任何语言内置类型都不足以胜任,数组无法容纳不同类型的元素。这也适用于STL类型(列表、向量、数组等)(除非它们具有多态相等性)。

因此,JSON中的任何容器都可以容纳任何类型的json类型,这就是全部内容。

我尝试过的原型或尝试过的方法以及为什么行不通 我的第一个天真的想法是只使用模板,因此我设置了一个json对象或json节点类型,然后使用模板来决定其中的内容,因此它将具有以下结构:

template <class T>
class JSONNode {
    const char *key;
    T value;
}

虽然这看起来很有前途,但当我开始使用它时,我意识到当我尝试将节点排序到容器类型(例如数组、向量、无序映射等)中时遇到了问题,因为它们仍然想知道那个JSONNode的类型! 如果一个节点被定义为 JSONNode<int> 而另一个节点被定义为 JSONNode<float> 那么将会有问题将它们放在一个容器中。
所以我不再关心将它们保留在容器中,我更愿意让它们自我感知或称之为添加指向下一个节点的指针,但是要找出节点的类型变得棘手,就在这里,我开始考虑多态性。
多态性 让我们制作一个虚拟的 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?那么在那一点上,我感觉变得太过复杂了,我想我可能能够使用typeof或decltype来确定需要强制转换的内容,但并未成功。
POD类型 因此,我尝试了不同的方法,认为也许我实际上可以通过pod方式实现这一点。然后,我将设置“value”部分为“void *”,并尝试保留一些“union”来跟踪类型。但是,我遇到了与之前相同的问题,即如何将数据强制转换为类型。
我感到有必要解释一下为什么我没有深入研究过POD的原因。
因此,如果有人对如何在C++中表示JSON具有智能解决方案,并且给出了这些信息,我将非常感激。

http://stackoverflow.com/questions/245973/whats-the-best-c-json-parser - Ashalynd
4
@Ashalynd提到,原帖作者非常强调他不对外部库感兴趣。 - JBentley
2
@Manu343726 不相关。原帖并没有询问是否应该重新发明轮子的建议。他已经表明了他想这样做,并且正在寻求如何实现的建议。 - JBentley
4
请参考boost::variant。如果您想重新发明它,请至少检查其设计。 - zch
@JBentley 我的评论只是一些建议,但如果 OP 想这样做,我很乐意回答问题并帮助他。 - Manu343726
显示剩余4条评论
6个回答

12

我认为你的最后方案是正确的,但我认为需要改变一些概念设计。

在我迄今为止使用过的所有JSON解析器中,选择容器类型的决定都是由用户而不是解析器做出的,我认为这是一个明智的决定,为什么呢?假设你有一个包含字符串格式数字的节点:

{
    "mambo_number": "5"
}

您不知道用户是否想将值作为字符串或数字进行检索。因此,我会指出JSONNumberNodeJSONStringNode并不适合最佳方法。我的建议是创建用于保存对象、数组和原始值的节点。

所有这些节点都包含一个标签(名称)和一个根据其主要类型嵌套对象列表:

  • JSONNode:包含节点键和类型的基本节点类。
  • JSONValueNode:管理和包含原始值的节点类型,例如上面列出的Mambo nº5,它将提供一些函数来读取其值,例如value_as_string()value_as_int()value_as_long()等。
  • JSONArrayNode:管理JSON数组并包含可通过索引访问的JSONNode
  • JSONObjectNode:管理JSON对象并包含可通过名称访问的JSONNode

我不知道这个想法是否有充分的文档支持,让我们看几个例子:

示例1

{
    "name": "murray",
    "birthYear": 1980
}

上面的JSON将是一个未命名的根JSONObjectNode,其中包含两个带有标签namebirthYearJSONValueNode

例子2

{
    "name": "murray",
    "birthYear": 1980,
    "fibonacci": [1, 1, 2, 3, 5, 8, 13, 21]
}

上面的JSON是一个未命名的根JSONObjectNode,其中包含两个JSONValueNode和一个JSONArrayNodeJSONArrayNode将包含8个未命名的JSONObjectNode,这些节点包含斐波那契序列的前8个值。
{
    "person": { "name": "Fibonacci", "sex": "male" },
    "fibonacci": [1, 1, 2, 3, 5, 8, 13, 21]
}

上面的JSON将是一个未命名的根JSONObjectNode,包含一个带有标签namesexJSONObjectNode和一个JSONArrayNode中包含两个JSONValueNode
{
    "random_stuff": [ { "name": "Fibonacci", "sex": "male" }, "random", 9],
    "fibonacci": [1, 1, 2, 3, 5, 8, 13, 21]
}

上面的JSON将是一个未命名的根JSONObjectNode,其中包含两个JSONArrayNode,第一个标记为random_stuff的数组将包含3个未命名的JSONValueNode,它们将按照出现的顺序依次是JSONObjectNodeJSONValueNodeJSONValueNode,第二个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;
}

为了将Array Node用作数组,必须提供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;
}

我认为Object Node必须提供带有字符串输入的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;
}

用法

假设所有的节点都具备所需的功能,我这种方法的使用思路将是:

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;

我认为值得创建每个对象的null版本,以便在无效访问的情况下提供:

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

前提是:当访问对象节点中不存在的子对象或越界访问数组节点时,返回空对象类型。
希望这有所帮助。

谢谢,这对我很有帮助,特别是因为你提供了一个非常易于理解的设计/概念和方法。我真的很感激你花时间做得如此详细和全面。 - qrikko
我来晚了,但是 JSONValueNodeJSONArrayNode 类的构造函数不应该与类名相同吗? - Nicholas Humphrey
1
@NicholasHumphrey,你来晚了,但你完全正确。我有一个复制粘贴的错误,我现在会立即修复它。非常感谢! - PaperBirdMaster
@Paula_plus_plus 感谢你的帖子。我打算在C++中编写一个小的JSON解析器,你的帖子绝对会对我有很大帮助! - Nicholas Humphrey
@Paula_plus_plus,我可以问你一个问题吗?在JSONNode中,似乎将std::string key用作JSONObject的“处理程序”,因此我们不需要使用映射容器,例如std::unordered_map。但是对于其他没有键的情况(即不在JSONObject中),您会将键留为空字符串,还是我对算法有误解? - Nicholas Humphrey
哇,你让我看了五年前的代码!我几乎不记得我当时写的任何东西了。但无论如何,你是对的:JSONNode::key 可能为 null(空字符串),因为有时 JSON 节点是匿名的(即:JSON 数组中的节点)。如果你查看对象和数组的实现,它们并没有使用任何映射(那可能会很慢!也许使用映射可以加快速度!)而是使用了 std::vector,这是因为我认为将 JSON 结构保存为它展示的样子可能是个好主意,保留顺序。 - PaperBirdMaster

8
你最后的两种解决方案都能起到作用。但是它们的问题似乎在于提取实际值,因此我们来看一些例子。我将介绍POD的想法,原因很简单,使用多态确实需要RTTI,而在我看来这是很丑陋的。
JSON:
{
    "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;
        }
    }

我会使用自由函数进行下转换,但还是加1。 - Basilevs
这与我使用POD(来自我的问题)解决方案的方式非常相似。我找不到进行“打字”的方法,我确信这实际上提供了一个解决方案。感谢您抽出时间阅读它。 - qrikko

4
我知道你说你对库不感兴趣,但是我曾经使用C++编写过一个解码/编码JSON的库:https://github.com/eteran/cpp-json。这是一个非常小的头文件库,因此您可以从中了解我的策略。
基本上,我有一个包装了boost::variant的json::value,因此它可以是基本类型之一(string、number、boolean、null),当然也可以是array或object。
由于array和object包含value,而value又可以是array和object,因此存在一些前向声明和动态分配的技巧。但这是一个大体的思路。
希望这能帮到你。

谢谢,我很感激,我不反对使用库,当像这样小型的库作为参考时会很有帮助。 - qrikko

1
我已经为JSON解析器编写了一个库。由模板类json::value实现的JSON表示符合C++标准库。它需要C++11和符合标准的容器。
JSON值基于一个类json::variant。这个类与boost::variant v1.52类似,但使用了更现代的实现(利用可变参数模板)。这个变量实现更加简洁,尽管由于广泛应用的模板技术而不太简单。它只有一个文件,而boost::variant的实现似乎过于复杂(由于缺乏可变参数模板而设计)。此外,json::variant在可能的情况下利用移动语义,并实现了一些技巧,以变得非常高效(优化代码比boost 1.53的代码快得多)。
json::value定义了一些其他类型,表示原始类型(数字,布尔,字符串,空值)。对象和数组容器类型将通过必须是符合标准的容器的模板参数来定义。因此,基本上可以在几个标准库兼容的容器中进行选择。
最后,JSON值包装一个变量成员并提供了一些成员函数和良好的API,使得使用JSON表示变得非常容易。
实现有一些不错的特性。例如,它支持“作用域分配器”。使用它,可以在构建JSON表示时使用“Arena Allocator”以提高性能。这需要一个符合并完全实现了支持作用域分配器模型的容器库(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 << "\n\n" << 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”状态,但基于C++核心实现(即解析器)的完整Objective-C包装器可以被视为最终版。不过,C++表示(json::value)仍需要一些工作才能完成。
尽管如此,该库可能是您想法的来源:代码位于GitHub上:JPJson,特别是文件夹Source/json/utility/中的variant.hppmpl.hpp以及文件夹Source/json/value/Source/json/generator/中的所有文件。
实现技术和源代码量可能会让人崩溃,并且仅在iOS和Mac OS X上使用现代clang进行了测试/编译 - 只是提醒一下;)

1

0

我会实现一个简化的boost::variant,其中只包含4种类型:一个unordered_map,一个vector,一个string和(可选)一个数字类型(我们需要无限精度吗?)。

每个容器都将包含指向相同类型实例的智能指针。

boost::variant存储了它所持有的类型的union,以及一个enum或索引作为其类型。我们可以询问它的类型索引,我们可以询问它是否具有特定类型,或者我们可以编写一个带有不同重载的访问者,variant将分派正确的调用。(最后一个是apply_visitor)。

我会模仿那个接口,因为我发现它既有用又相对完整。简而言之,重新实现一部分boost,然后使用它。请注意,variant是一个仅头文件类型,因此可能足够轻量级,只需包含即可。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接