Типы данных для представления JSON в С++
Я пытался понять это сейчас, и, может быть, я просто слишком долго смотрел на него?
Во всяком случае проблема заключается в том, чтобы найти хороший способ представления JSON в С++ и, прежде чем вы прочтете больше, обратите внимание, что меня не интересуют библиотеки, способные на это, поэтому я хочу сделать это в raw C или С++ (С++ 11 в порядке), нет boost, no libjson, я знаю о них и по причинам, выходящим за рамки этого вопроса, я не могу (/wont) добавлять зависимости.
Теперь, когда это прояснилось, позвольте мне рассказать вам немного о проблеме и о том, что я пробовал до сих пор.
Проблема заключается в том, чтобы найти хороший способ представления JSON в С++, причина, по которой это немного проблематично, заключается в том, что JSON является супер-слабо типизированным, а С++ действительно жестко типизирован. рассмотрите JSON на секунду, что такое JSON, действительно способный типично?
- Число (например,
42
или 3.1415
)
- Строка (например,
"my string"
)
- Массив (например,
[]
или [1,3.1415,"my string]
)
- Объект (например,
{}
или {42, 3.1415, "my string", [], [1,3.1415, "my string]}
Итак, это означает, что существуют два "необработанных" типа: Число и Строка, а два типа контейнеров Массив и Объект. Необработанные типы довольно прямолинейны, в то время как типы контейнеров становятся сложными на C/С++, поскольку они могут и, вероятно, будут содержать элементы разных типов, поэтому любой встроенный тип на языке не будет достаточным, как есть, массив не может содержать элементы разных типов. Это справедливо и для STL-типов (список, вектор, массив и т.д.) (Если они не имеют полиморфного равенства).
Таким образом, любой контейнер в JSON может содержать любой тип json-типа, который в значительной степени подходит для него.
Что я прототипировал или пытался и почему это не работает
Моя первая наивная мысль заключалась в том, чтобы просто использовать шаблоны, поэтому я настроил тип json-object или json- node, который затем использовал бы шаблоны, чтобы решить, что в нем, поэтому у него будет структура вроде этого:
template <class T>
class JSONNode {
const char *key;
T value;
}
Хотя это казалось многообещающим, однако, когда я начал работать с ним, я понял, что столкнулся с проблемами, когда пытался упорядочить узлы в виде контейнера (например, array, vector, unordered_map и т.д.), поскольку они все еще хотите знать тип этого JSONNode! если один node определяется как JSONNode<int>
, а другой - JSONNode<float>
ну, тогда будет проблематично иметь их в контейнере.
Итак, я продвигаюсь мимо этого, я все равно не заинтересован в том, чтобы держать их в контейнере, я был бы счастлив сделать их самосознающими или как их называть, т.е. объявление в указателе на следующий node, но опять-таки становится сложно определить тип node, и примерно здесь, когда я начинаю думать о полиморфизме.
Полиморфизм
Позвольте просто создать виртуальный 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;
}
Сначала я думал, что это путь. Однако, когда я начал думать о том, как обрабатывать элемент value, я понял, что не могу получить доступ к значению, даже если я написал определенную функцию для получения значения, что бы он вернулся?
Таким образом, я уверен, что у меня есть объекты с разными типизированными значениями, но я не могу получить к ним доступ без первого приведения к правильному типу, поэтому я мог бы сделать dynamic_cast<JSONStringNode>(some_node);
, но как бы я знал, к чему это можно привести? RTTI? Ну, я чувствую, что в этот момент мне становится сложнее, я думаю, я мог бы использовать typeof или decltype, выясняя, что придумать, но не удалось.
Типы POD
Поэтому я попробовал что-то другое, я подумал, что могу утверждать, что, возможно, я мог бы сделать это в корме. Затем я установил бы часть value
как void *
и попробовал бы некоторые union
отслеживать типы. Однако я получаю ту же проблему, что и у меня, а именно, как передавать данные в типы.
Я чувствую необходимость обернуть этот вопрос, почему я не углубился в то, что я пытался использовать POD..
Итак, если у кого-то есть умное решение о том, как представлять JSON в С++, учитывая эту информацию, я был бы так благодарен.
Ответы
Ответ 1
Ваши последние два решения будут работать. Ваша проблема в обоих из них, по-видимому, извлекает фактические значения, поэтому давайте рассмотрим примеры. Я расскажу об идее POD по той простой причине, что использование полиморфности действительно потребует RTTI, который IMHO является уродливым.
JSON:
{
"foo":5
}
Вы загружаете этот JSON файл, то, что вы получите, - это просто ваша обложка POD.
json_wrapper wrapper = load_file("example.json");
Теперь вы предполагаете, что загруженный JSON node является объектом JSON. Теперь вам придется обрабатывать две ситуации: либо это объект, либо нет. Если это не так, вы, скорее всего, попадете в состояние ошибки, поэтому могут быть использованы исключения. Но как бы вы сами извлекли объект? Ну, просто с вызовом функции.
try {
JsonObject root = wrapper.as_object();
} catch(JSONReadException e) {
std::cerr << "Something went wrong!" << std::endl;
}
Теперь, если JSON node, завернутый в wrapper
, действительно является объектом 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;
}
}
Ответ 2
Я думаю, что вы идете в правильном направлении с помощью своего последнего подхода, но я думаю, что ему нужно изменить некоторые концепции dessigns.
Во всех анализаторах JSON, над которыми я работал до сих пор, решение о выборе типа контейнера было на стороне пользователя не на стороне анализатора, и я думаю, что это мудрое решение, почему? предположим, что у вас есть node, который содержит число в строчном формате:
{
"mambo_number": "5"
}
Вы не знаете, хочет ли пользователь получить значение в виде строки или числа. Итак, я укажу, что JSONNumberNode
и JSONStringNode
не подходят для наилучшего подхода. Мой совет - создать узлы для хранения объектов, массивов и необработанных значений.
Все эти узлы будут содержать метку (имя) и список вложенных объектов в соответствии с ее основным типом:
-
JSONNode
: базовый класс node, содержащий ключ и тип node.
-
JSONValueNode
: тип node, который управляет и содержит необработанные значения, такие как Mambo nº5, перечисленные выше, он предоставит некоторые функции для считывания его значения, например value_as_string()
, value_as_int()
, value_as_long()
и до сих пор...
-
JSONArrayNode
: тип node, который управляет массивами JSON и содержит JSONNode
accessibles по индексу.
-
JSONObjectNode
: тип node, который управляет объектами JSON и содержит JSONNode
, доступный по имени.
Я не знаю, хорошо ли задокументирована идея, давайте посмотрим несколько примеров:
Пример 1
{
"name": "murray",
"birthYear": 1980
}
JSON выше будет безымянным корнем JSONObjectNode
, который содержит два JSONValueNode
с метками name
и birthYear
.
Пример 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
, который содержит JSONObjectNode
с двумя JSONValueNode
с метками name
и sex
и одним JSONArrayNode
.
Пример 4
{
"random_stuff": [ { "name": "Fibonacci", "sex": "male" }, "random", 9],
"fibonacci": [1, 1, 2, 3, 5, 8, 13, 21]
}
JSON выше будет безымянным корнем JSONObjectNode
, который содержит два JSONArrayNode
, первый, помеченный как random_stuff
, будет содержать 3 неназванных JSONValueNode
, которые будут иметь тип JSONObjectNode
, JSONValueNode
и JSONValueNode
в порядке появления, вторая JSONArrayNode
- это последовательность фибоначчи, прокомментированная ранее.
Реализация
То, как я столкнулся с реализацией узлов, будет следующим:
База node будет знать свой собственный тип (значение Node, массив node или объект Node) через член 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;
};
Производные классы должны предоставить базовому типу node во время построения, значение node предоставляет пользователю преобразование сохраненного значения в тип запроса пользователя:
class JSONValueNode : JSONNode {
public:
JSONObjectNode(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;
}
Массив node должен предоставить operator[]
, чтобы использовать его как массив; реализовать некоторые итераторы было бы полезно. Сохраненные значения внутреннего std::vector
(выберите контейнер, который вы считаете лучшим для этой цели) были бы JSONNode
.
class JSONArrayNode : JSONNode {
public:
JSONObjectNode(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[]
строковый ввод, потому что в С++ мы не можем реплицировать аксессуар 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 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;
Я думаю, стоит создать нулевые версии каждого объекта для предоставления в случае недопустимых acces:
// 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
}
Предпосылка: возвращает нулевой тип объекта в случае присоединения несуществующего под-объекта в объекте node или внеочередной доступ к массиву node.
Надеюсь, что это поможет.
Ответ 3
Я знаю, что вы сказали, что вас не интересуют библиотеки, но я сделал один в прошлом для декодирования/кодирования JSON с использованием С++:
https://code.google.com/p/cpp-json/
Это довольно маленькая библиотека, которая является только заголовком, поэтому вы можете извлечь из нее мою стратегию.
По существу, у меня есть json::value
, который обертывает boost::variant
, поэтому он может быть либо одним из основных типов (string
, number
, boolean
, null
), либо он также может быть a array
или object
, конечно.
Существует несколько сложностей с форвардными объявлениями и динамическим распределением, поскольку array
и object
содержат value
s, что в свою очередь может быть array
и object
s. Но эта общая идея.
Надеюсь, что это поможет.
Ответ 4
Если вы заинтересованы в обучении, я настоятельно рекомендую чтение через источник jq - это действительно чистый C-код без внешних зависимостей библиотеки json.
Внутренне jq хранит информацию о типе в простом перечислении, что устраняет большинство проблем типа компиляции. Хотя это означает, что вам необходимо создать основные операции.
Ответ 5
Я написал библиотеку для парсера JSON. Представление JSON, которое реализуется классом шаблона json::value
, соответствует стандартной библиотеке С++. Это требует С++ 11 и стандартных контейнеров.
Значение JSON основано на классе json::variant
. Это не похоже на boost::variant
v1.52, но использует более современную реализацию (используя вариативные шаблоны). Эта реализация варианта намного более кратка, хотя и из-за повсеместно применяемых методов шаблонов не совсем проста. Это всего лишь один файл, а реализация boost::variant
представляется излишне сложной (из-за отсутствия вариационных шаблонов, поскольку она была разработана). Кроме того, json:: variant использует семантику перемещения, где это возможно, и реализует несколько трюков, чтобы стать достаточно результативными (оптимизированный код значительно быстрее, чем у boost 1.53).
Класс json::value
определяет несколько других типов, представляющих примитивные типы (Number, Boolean, String, Null). Типы контейнеров Object и Array будут определяться параметрами шаблона, которые должны быть стандартными контейнерами-контейнерами. Таким образом, в основном можно выбрать один из нескольких стандартных контейнеров, совместимых с lib.
Наконец, значение JSON обертывает вариантный член и предоставляет несколько функций-членов и хороший API, что упрощает использование представления JSON.
Реализация имеет несколько приятных функций. Например, он поддерживает "Scoped Allocators". С его помощью становится возможным использовать "Арена Алокатор" для повышения производительности при построении представления 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 << "\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
. Парсер сильно оптимизирован для скорости и низкой занимаемой памяти.
Пока я рассматриваю состояние представления С++ (json::value
) по-прежнему как "Alpha", существует полная оболочка Objective-C, которая основана на основной реализации С++ (а именно, анализаторе), которая может быть рассмотрена окончательный. Однако представление С++ (json::value
) все еще нуждается в некоторой работе для завершения.
Тем не менее, библиотека может быть источником ваших идей: код находится в GitHub: JPJson, особенно файлы variant.hpp
и mpl.hpp
в папке Source/json/utility/
и всех файлах в папке Source/json/value/
и Source/json/generator/
.
Методы реализации и объем исходного кода могут быть разгромлены и были протестированы/скомпилированы только с помощью современного clang на iOS и Mac OS X - просто нужно предупредить;)
Ответ 6
Я бы реализовал упрощенную boost::variant
с только 4-мя типами в ней: unordered_map
, a vector
, a string
и (необязательно) числовой тип (нам нужна бесконечная точность?).
Каждый из контейнеров будет содержать интеллектуальные указатели на экземпляры одного и того же типа.
boost::variant
хранит union
по типам, которые он содержит, и enum
или index asto, который имеет тип. Мы можем запросить его для индекса типа, мы можем спросить его, имеет ли он один тип i, или мы можем записать посетителя с переопределениями, которые variant
отправляет правильный вызов. (последнее - apply_visitor
).
Я бы подражал этому интерфейсу, потому что нашел его полезным и относительно полным. Короче говоря, повторно реализуйте часть boost
, а затем используйте это. Обратите внимание, что variant
является только типом заголовка, поэтому он может быть достаточно легким, чтобы просто включать.