Хорошо в плане поддержки JSON живётся программистам на Javascript — по какому-то невероятному стечению обстоятельств там JSON входит в спецификацию самого языка: есть JSON — есть объект. Удобно. Неплохо дело обстоит и в языках, где JSON не входит в сам язык, но поддерживается стандартной библиотекой (Python, Ruby): импортируешь модуль — и готово.
Жизнь программистов на С++ никогда не была особо простой — поддержки JSON у нас нет ни на уровне языка, ни в стандартной библиотеке. И не будет, возможно, никогда. «Тоже мне проблему нашел!» — скажут мне опытные коллеги — «Её там и не должно быть, С++ поставляется без „батареек“. Для решения этой задачи мы...» и вот здесь они разделятся на два лагеря:
1. «Мы используем большой фреймворк (boost, Qt, POCO, другой), который применяется во всех наших проектах и умеет 150 000 разных вещей, в том числе и JSON.»
2. «Мы придерживаемся подхода в котором для каждой задачи применяется своя легковесная библиотека. В частности, для JSON мы уже 150 000 лет назад выбрали отличную библиотеку %JSON_LIB%, которая прекрасно работает.»
Да, всё так и есть. Вот только…
Давайте немного помечтаем.
А что, если бы JSON вошел в стандартную библиотеку нового стандарта С++? Что, если бы он был написан в терминах С++11\14 и без требований обратной совместимости со старыми стандартами языка? Что, если бы синтаксис этого модуля попытались бы сделать максимально приближенным к родному для JSON использованию «а-ля Javascript», но в том же время сохранить дух С++ (эффективность, минимальное потребление памяти, совместимость с STL)? Что, если бы его можно было включить в проект одним инклюдом и не беспокоиться о его сборке и линковке? Как бы это всё выглядело и работало?
И у нас есть ответ на этот вопрос! Давайте посмотрим на JSON-библиотеку для С++ написанную в соответствии со всеми этими принципами, ну и вообще написанной людьми для людей, а не чужими для хищников, как это обычно бывает.
Итак, пусть у нам нужно создать вот такой JSON-объект:
Качаем github.com/nlohmann/json/blob/master/src/json.hpp, включаем его инклюдом в cpp-файл.
Кстати, используя последнюю фичу, вместо всего этого можно было бы написать создания объекта очень похожее на сам JSON-формат:
А вот они.
Ну а ещё есть поддержка создания JSON из любых STL-контейнеров (std::array, std::vector, std::deque, std::forward_list, std::list, std::set, std::multiset, std::unordered_set, std::unordered_multiset). Для линейно-упорядоченных и упорядоченных ассоциативных контейнеров порядок элементов будет сохранён, для unordered-ассоциативных, понятное дело — нет.
А ещё есть 100% покрытие библиотеки тестами и гарантия отсутствия утечек памяти от Valgrind.
В общем, я понимаю, что это «всего-лишь ещё одна библиотека для поддержки JSON» и по сути ничем она от миллиона других не отличается. Но как же всё-таки с ней удобно работать!
Жизнь программистов на С++ никогда не была особо простой — поддержки JSON у нас нет ни на уровне языка, ни в стандартной библиотеке. И не будет, возможно, никогда. «Тоже мне проблему нашел!» — скажут мне опытные коллеги — «Её там и не должно быть, С++ поставляется без „батареек“. Для решения этой задачи мы...» и вот здесь они разделятся на два лагеря:
1. «Мы используем большой фреймворк (boost, Qt, POCO, другой), который применяется во всех наших проектах и умеет 150 000 разных вещей, в том числе и JSON.»
2. «Мы придерживаемся подхода в котором для каждой задачи применяется своя легковесная библиотека. В частности, для JSON мы уже 150 000 лет назад выбрали отличную библиотеку %JSON_LIB%, которая прекрасно работает.»
Да, всё так и есть. Вот только…
Чем плох подход с использованием фреймворков
Во-первых, тянуть в проект огромный фреймворк ради одного JSON — как-то уныло. Ну ладно, допустим фреймворк у вас был и так. Но тогда придётся писать работу с JSON в терминах фреймворка, а это, как правило, тихий ужас. Посмотрите, например, на документацию по JSON в Qt — куча собственных типов вроде QJsonArray, QJsonDocument, QJsonObject, QJsonValue и т.д. и их придётся использовать. О том, чтобы потом перенести код в другой проект (где этого фреймворка нет) можно сразу забыть. Ну или вот Boost: парсер JSON находится очень логично в модуле Boost.PropertyTree. Ага, так бы я и догадался. Т.е. нам предлагают плясать не от формата JSON, а от структуры данных «дерево», которая умеет себя читать в том числе и из JSON.
В общем, фреймворки навязывают нам своё виденье задачи, свой способ её решения и стремятся навсегда привязать нас к себе. Нет, если вы уверены, что нашли тот самый единственный и неповторимый фреймворк и будете с ним счастливы до конца жизни — воля ваша. Но я как-то не сторонник подобного фатализма.
В общем, фреймворки навязывают нам своё виденье задачи, свой способ её решения и стремятся навсегда привязать нас к себе. Нет, если вы уверены, что нашли тот самый единственный и неповторимый фреймворк и будете с ним счастливы до конца жизни — воля ваша. Но я как-то не сторонник подобного фатализма.
Чем плох подход с использованием библиотек
Плох он вот этой частью: "...150 000 лет назад выбрали отличную библиотеку...". Скорее всего речь идёт о чём-то, что начинало писаться чуть-ли не во времена DOSа и, без сомнения, работает, но при этом, пытаясь быть совместимым со всеми платформами и стандартами языка совершенно отстаёт от прогресса. Да, всё компилируется и работает, даже тесты проходит. Но библиотека совершенно не знакома с такими вещами, как ключевое слово auto, range-based циклы, строковые литералы, raw-строки, конструкторы перемещения, списки инициализации и прочие классные вещи, делающие код одновременно более эффективным и более легко читаемым. А ведь у библиотеки, созданной годы назад, есть обязательства по обратной совместимости, а значит просто так взять и добавить это всё она не может.
Давайте немного помечтаем.
А что, если бы JSON вошел в стандартную библиотеку нового стандарта С++? Что, если бы он был написан в терминах С++11\14 и без требований обратной совместимости со старыми стандартами языка? Что, если бы синтаксис этого модуля попытались бы сделать максимально приближенным к родному для JSON использованию «а-ля Javascript», но в том же время сохранить дух С++ (эффективность, минимальное потребление памяти, совместимость с STL)? Что, если бы его можно было включить в проект одним инклюдом и не беспокоиться о его сборке и линковке? Как бы это всё выглядело и работало?
И у нас есть ответ на этот вопрос! Давайте посмотрим на JSON-библиотеку для С++ написанную в соответствии со всеми этими принципами, ну и вообще написанной людьми для людей, а не чужими для хищников, как это обычно бывает.
Итак, пусть у нам нужно создать вот такой JSON-объект:
{
"name": "Habrahabr",
"nothing": null,
"answer": {
"everything": 42
},
"companies": ["Infopulse", "TM"],
"user": {
"name": "tangro",
"active": true
}
}
Давайте возьмём и создадим!
Качаем github.com/nlohmann/json/blob/master/src/json.hpp, включаем его инклюдом в cpp-файл.
#include "json.hpp"
using json = nlohmann::json;
// создаём пустой JSON-объект
json j;
// добавляем строку, которая будет храниться как std::string
j["name"] = "Habrahabr";
// добавляем пустой вложенный объект
j["nothing"] = nullptr;
// число внутри вложенного объекта
j["answer"]["everything"] = 42;
// добавляем массив строк (будет храниться как std::vector<std::string>)
// обратите внимание - используются списки инициализации
j["companies"] = { "Infopulse", "TM" };
// добавляем ещё один объект - на этот раз используем список инициализации с парами "ключ"-"значение"
j["user"] = { {"name", "tangro"}, {"active", true} };
Кстати, используя последнюю фичу, вместо всего этого можно было бы написать создания объекта очень похожее на сам JSON-формат:
json j2 =
{
{"name", "Habrahabr"},
{"nothing", nullptr},
{"answer", {
{"everything", 42}
}},
{"companies", {"Infopulse", "TM"}},
{"user", {
{"name", "tangro"},
{"active", true}
}}
}
Так, а где же все эти модные фишки С++11?
А вот они.
// создание объекта из строкового литерала
json j = "{ \"active\": true, \"pi\": 3.141 }"_json;
// использование raw string
json j = R"(
{
"active": true,
"pi": 3.141
}
)"_json;
// использование auto
auto j = json::parse("{ \"active\": true, \"pi\": 3.141 }");
// использование range-based for
for (auto element : j) {
std::cout << element << '\n';
}
Так, ладно, я приверженец «классического» стиля, покажите мне совместимость с STL
// создание массива, синтаксис "а-ля std::vector"
json j;
j.push_back("foo");
j.push_back(1);
j.push_back(true);
// создание объекта, синтаксис "а-ля std::map"
json o;
o["foo"] = 23;
o["bar"] = false;
// поиск элемента
if (o.find("foo") != o.end()) {
// найдено
}
// итерация по массиву
for (json::iterator it = j.begin(); it != j.end(); ++it) {
std::cout << *it << '\n';
}
// перегруженный оператор [] и метод at(), поддержка std::string
const std::string tmp = j[0];
j[1] = 42;
bool foo = j.at(2);
// знакомые методы работы с контейнерами
j.size(); // 3 элемента
j.empty(); // true или false
j.type(); // json::value_t::array
j.clear(); // очищаем
// дамп в строку
std::string s = j.dump();
Ну а ещё есть поддержка создания JSON из любых STL-контейнеров (std::array, std::vector, std::deque, std::forward_list, std::list, std::set, std::multiset, std::unordered_set, std::unordered_multiset). Для линейно-упорядоченных и упорядоченных ассоциативных контейнеров порядок элементов будет сохранён, для unordered-ассоциативных, понятное дело — нет.
Так, почти убедил
А ещё есть 100% покрытие библиотеки тестами и гарантия отсутствия утечек памяти от Valgrind.
Вот и всё.
В общем, я понимаю, что это «всего-лишь ещё одна библиотека для поддержки JSON» и по сути ничем она от миллиона других не отличается. Но как же всё-таки с ней удобно работать!