JSON-сериализатор на швидких шаблонів



В чому проблема текстових форматів обміну даними? Вони повільні. І не просто повільні, а жахливо повільні. Так, вони надлишкові, порівняно з бінарними протоколами і, по ідеї, текстовий сериализатор повинен бути повільніше приблизно на стільки ж, на скільки він надмірний. Але на практиці виходить, що текстові сериализаторы інший раз на порядки поступаються бінарним аналогам.

Я не буду розмірковувати про переваги JSON перед бінарними форматами — у кожного формату є своя область застосування, в якій він хороший. Але найчастіше ми змушені відмовлятися від чогось зручного в користь не дуже комфортного внаслідок катастрофічної неефективності першого. Розробники відмовляються від JSON, навіть якщо він прекрасно підходить для розв'язання задачі, тільки з-за того, що він виявляється вузьким місцем у системі. Звичайно ж, винен не JSON сам по собі, а реалізації відповідних бібліотек.

У цій статті я розповім не тільки про проблеми парсерів текстових форматів взагалі і JSON зокрема, але і про нашу бібліотеку, яку ми використовуємо вже багато років в самих високонавантажених проектах. Вона настільки нас влаштовує і в плані швидкодії, і в плані зручності використання, що часом відмовляємося від бінарного формату там, де б він більше підійшов. Звичайно ж, я маю на увазі якісь прикордонні умови, без претензій на всі випадки життя.



Для оцінки масштабу трагедії я заміряв час серіалізації не сильно складного об'єкта для пари відомих JSON-бібліотек, google::protobuf, «ручний» JSON-серіалізації та для бібліотеки wjson, розробником якої я є і розповім докладно в цій статті далі.

Результати показані на діаграмі:

Зізнаюся, мене ці результати в свій час, скажімо м'яко, злегка здивували.

Продуктивність jsoncpp і json_spirit ( на базі boost::spirit ) катастрофічно програє google::protobuf. Ситуація з «ручний» серіалізацією з використанням sprintf/sscanf або std::stringstream істотно краще. Але якщо ви використовуєте перші два інструмента, то не поспішайте все кидати і з вигуком: «я ж казав, що треба робити самому!» — переробляти свої проекти. На графіку виміри для одного єдиного виклику sprintf/sscanf, в який ми запхали сериализуемый об'єкт без жодних перевірок і можливості переставляти або пропускати поля в JSON-об'єкті. Більш докладні цифри я наведу в розділі про серіалізації об'єктів.

У цій статті я розглядаю JSON як формат обміну повідомленнями з акцентом на продуктивність. Відповідно порівнюю ті чи інші технології саме в цьому контексті. Це означає також, що структура повідомлень на етапі розробки (компіляції), нам відома. Пропонована бібліотека wjson також розроблялася саме для цих завдань. Досліджувати невідомі JSON-документи за допомогою неї, звичайно ж, можна, і можливо, wjson буде ефективніше багатьох бібліотек, у всякому разі, jsoncpp і json_spirit — це точно.

Насправді wjson і концептуально ближче до protobuf, ніж, наприклад, до згаданих вище бібліотек. Він точно так само по деякому мета-опису генерує код серіалізації/десеріалізації. Але на відміну від protobuf не використовує зовнішню програму, а компілятор C++. Я в попередній статті показав, як компілятор можна навчити грати в хрестики-нулики, а навчити його генерувати код серіалізації — справа техніки.

Але що мені самому подобається, так це те, що в сериализуемые структури даних не потрібно впроваджувати ніякого додаткового функціоналу — мухи окремо, котлети окремо. А ще підтримується спадкування, в тому числі і множинне, агрегація будь-якої вкладеності, і деякі плюшки, наприклад, сериализация enum-ів.

Спочатку wjson замислювалася виключно для декларативного опису JSON-конструкцій на шаблонах c++, щоб позбавити програміста від написання run-time коду з безліччю необхідних перевірок. Але швидко з'ясувалося, що компілятор агресивно inline-іт подібні конструкції. І знадобилося зовсім небагато зусиль, щоб змусити ці конструкції працювати достатньо ефективно і вийти на прийнятний рівень продуктивності.

Так чому ж JSON такий повільний?Якщо ви працювали з XML, то ви в курсі, що є два підходи до десеріалізації — це DOM (Document Object Model) і SAX(Simple API for XML). Нагадаю, що у разі DOM текст перетворюється в дерево вузлів, яке можна досліджувати, використовуючи відповідне API. А SAX парсер працює по-іншому — він сканує документ і генерує ті чи інші події, які обробляє код користувача, реалізований, як правило, у вигляді функцій зворотного виклику. Так чи інакше, більшість текстових десериализаторов використовують один з цих підходів, або комбінують їх.

Основний недолік DOM в необхідності будувати дерево, а це завжди непросто. Процес побудови такого дерева на етапі десеріалізації може відбуватися істотно довше, ніж виконання прикладних алгоритмів. Але крім цього, необхідно здійснити пошук необхідних полів і перетворити їх у внутрішні структури даних. А це завдання лягає вже на плечі програміста, яку він може реалізувати надзвичайно неефективно. Насправді це вже не настільки важливо, тому що саме побудова дерев DOM з'їдає основні ресурси.

Парсери типу SAX набагато швидше. За великим рахунком вони проходять текст один раз, викликаючи відповідні хандлеры. Реалізувати подібний парсер для JSON тривіальна задача, тому як сам JSON простий до неподобства, і в цьому його краса. Але вимагає від програміста, його використовує, набагато більше зусиль по вилученню даних. А це — більше роботи програмісту, більше помилок і неефективного коду, що може звести нанівець ефективність SAX.

Взагалі, тема ефективної серіалізації дуже цікава з точки зору розробки якогось універсального рішення. Але з позиції прикладного програмування — це надзвичайно нудно і нудно, а як наслідок, величезні обсяги неефективного говнокода. Програмісту набагато цікавіше оптимізувати прикладної код, а те, що десериализация відбувається на порядок повільніше і його оптимізація в цьому контексті не має особливого сенсу, його мало турбує.

Але якщо більшість існуючих рішень з будь-яких причин нас не влаштовує, давайте зробимо це уручну, благо JSON дуже простий формат. А тут дуже цікаву історію я спостерігав кілька разів. Якщо програміст працює з бінарними протоколами, він скрупульозно зрушує біти, оптимізує код і отримує від цього задоволення. А от якщо йому запропонувати попрацювати з текстовим форматом у цьому ж контексті, у нього в мозку як ніби відключається. Точніше, навпаки, у нього включається захист проти велосипедостроительства вищого порядку, від якої при роботі з бінарним форматом він не особливо страждав. Він підключає купи бібліотек для роботи з текстом заради пари гарних (але, як правило, не дуже ефективних) функцій, щоб якось облагородити свій код, який працює з JSON. А результат все одно плачевний.

Мало кому в голову прийде писати свою реалізацію atoi, але ми все ж спробуємо:

template < typename T, typename P> 
P my_atoi( T& v, P beg, P end) 
{ 
if( beg==end) return end; 

bool neg = ( *beg=='-' ); 
if ( neg ) ++beg; 
if ( beg == end || *beg < '0' || *beg > '9') return 0; 
if (*beg=='0') return ++beg; 

v = 0; 
for ( ;beg!=end; ++beg ) 
{ 
if (*beg < '0' || *beg > '9') break; 
v = v*10 + (*beg - '0'); 
} 

if (neg) 
v = static_cast<T>(-v); 

return beg; 
} 


Насправді все просто, але універсальніше і зручніше (на мій погляд), ніж класичне atoi. Але найцікавіше — це працює в два рази швидше. Так, звичайно, здебільшого за рахунок inline підстановки, але це не суть важливо. До речі, sscanf/sprintf відпрацьовують %s параметри швидше, ніж %d, при порівнянній довжині рядка.

Я не буду зараз розповідати про небезпеку sscanf/sprintf, про це вже писали багато раз, і, крім того, є безпечні альтернативи, наприклад, std::stringstream або boost::lexical_cast<>. На жаль, багато програмісти, в тому числі і C++, керуються міфом, що тру Сі швидше, і з завидною завзятістю починають використовувати sscanf/sprintf. Але проблема-то, в даному контексті, не в мові, а в реалізації того чи іншого функціоналу. Наприклад, std::stringstream, при правильному використанні, може бути й не гіршою Сі альтернатив, а ось, скажімо, boost::lexical_cast<> може істотно поступатися в цьому плані.

Тому потрібно ретельно тестувати продуктивність не тільки сторонні бібліотеки, але і знайомі інструменти. Але часто буде швидше свелосипедничать, підглянувши необхідні реалізації в Інтернеті.

Код для my_atoi практично без змін взято з wjson, може, кому згодиться. Код для серіалізації трохи більше заморочен:

itoa
// Обчислює розмір тимчасового буфера в залежності від типу 
template < typename T> 
struct integer_buffer_size 
{ 
enum { value = sizeof(T)*2 + sizeof(T)/2 + sizeof(T)%2 + is_signed_integer<T>::value }; 
}; 

// Перевірка на негативні значення для знакових 
template < typename T, int > 
struct is_signed_integer_base 
{ 
enum { value = 1 }; 
static bool is_less_zero(T v) { return v < 0; } 
}; 

// Для беззнакових перевірки немає, завжди false 
template < typename T> 
struct is_signed_integer_base<T, false> 
{ 
enum { value = 0 }; 
static bool is_less_zero(T ) { return false; } 
}; 

template < typename T> 
struct is_signed_integer: 
is_signed_integer_base< T, ( T(-1) < T(1) ) > 
{ 
}; 

template < typename T, typename P> 
P my_itoa(T v, P itr) 
{ 
char buf[integer_buffer_size<T>::value]; 
char *beg = buf; 
char *end = buf; 
if (v==0) 
*(end++) = '0'; 
else 
{ 
// для беззнакових типів умова вироджується if (false) 
// і непотрібний код оптимізатор прибирає. Також це прибирає 
// попередження компілятора для беззнакових типів 
if ( is_signed_integer<T>::is_less_zero(v) ) 
{ 
for( ; v!=0 ; ++end, v/=10) 
*end = '0' - v%10; 
*(end++)='-'; 
} 
else 
{ 
for( ; v!=0 ; ++end, v/=10) 
*end = '0' + v%10; 
} 
} 

do { *(itr++)=*(--end); } while( end != beg ); 

return itr; 
} 



За рахунок такого ось побайтного перебору для інших конструкцій JSON і inline підстановки можна домогтися більш швидкого десеріалізації. Якщо зібрати їх яким-небудь чином в єдину конструкцію, то отримаємо свого роду SAX-парсер, який до того ж дуже швидкий.


Прості типи



Давайте відразу приклад серіалізації:

int value = 12345; 
char bufjson[100]; 
char* ptr = wjson::value<int>::serializer()(value, bufjson); 
*ptr = '\0'; 
std::cout << bufjson << std::endl; 


Тут wjson::value<int> — це JSON опис цілочисельного типу, яке містить визначення сериализатора для цього типу. Далі ми створюємо об'єкт сериализатора і викликаємо перевантажений оператор (). Комусь такий запис може здатися дивною, але саме її будемо використовувати, щоб підкреслити, що об'єкт JSON-сериализатора не має стану і створювати його примірник не має сенсу.

Відразу відповім на питання, чому serializer не static-функція. По-перше, static-елементи компілятор не дуже любить в плані часу компіляції, а по-друге, це просто зручніше, у всякому разі, для мене. За фактом тут відбудеться повна підстановка коду, який я показав під спойлером вище, на прикладі my_itoa.

Конструкція value<> використовується не тільки для цілочислових, але і для речових, рядків і бульових. Визначення:
template < typename T, int R = -1> 
struct value 
{ 
typedef T target; 
typedef implementation_defined serializer; 
}; 

Для булевого і цілочисельних типів аргумент R не використовується. Для рядків типу std::string або std::vector<char> — це розмір резерву, а для речових — формат подання.

Клас serializer, крім серіалізації, надає функціонал десеріалізації, тобто два перевантажених operator():
template < typename T> 
class implementation_defined 
{ 
public: 
template < typename P> 
P operator()( const T& v, P itr); 

template < typename P> 
P operator() ( T& v, P beg, P end, json_error* e ); 
}; 

Функція серіалізації приймає на вхід, крім посилання на сериализуемый тип, output-ітератор, наприклад:
int value = 12345; 

std::string strjson; 
wjson::value<int>::serializer()(value, std::back_inserter(strjson)); 
std::cout << strjson << std::endl; 

std::stringstream ssjson; 
wjson::value<int>::serializer()(value, std::ostreambuf_iterator<char>(ssjson)); 
std::cout << ssjson.str() << std::endl; 

wjson::value<int>::serializer()(value, std::ostreambuf_iterator<char>(std::cout)); 
std::cout << std::endl; 

Десериализатор приймає на вхід ітератори довільного доступу, що вказують на початок і кінець буфера, а також вказівник на об'єкт помилки, який може бути нульовим:
value = 0; 
char bufjson[100]="12345"; 
wjson::value<int>::serializer()(value, bufjson, bufjson + strlen(bufjson), 0 ); 
std::cout << value << std::endl; 

value = 0; 
std::string strjson="12345"; 
wjson::value<int>::serializer()(value, strjson.begin(), strjson.end(), 0 ); 
std::cout << value << std::endl; 

І сериализатор, і десериализатор повертають ітератори. У першому випадку він вказує на місце, де закінчилася серіалізація, а в другому — місце у вхідному буфері, де закінчилася десериализация, якщо не було помилок. У разі помилки повертає покажчик на кінець буфера, і код помилки, якщо був переданий не нульовий покажчик. Про обробку помилок трохи пізніше, а зараз доб'ємо прості типи.

Підтримувані цілочисельні: char, unsigned char, short, unsigned short, int, unsigned int, long int, unsigned long, long long unsigned long long. C булевими (bool), все те ж саме, серіалізует в «true» або «false» і назад. Автоматичне перетворення інших типів при десеріалізації не підтримується.

Єдиний тип, з якими я не особливо морочився в плані продуктивності — це речовинний (float, double, long double), там звичайний std::stringstream. В першу чергу це пов'язано з тим, що в реальних проектах, з якими я працював, його завжди можна замінити або на цілочисельні типи (наприклад, передавати метри в міліметрах), або навантаження у межах 10К на ядро CPU, що не суттєво. Якщо у вас основний обсяг трафіку — це речові і ніяк від цього не піти, то має сенс заморочити з оптимізацією. За замовчуванням речові сериализуются з мантиссой. При R>=0, як з фіксованою комою:
double value = 12345.12345; 

std::string json; 
wjson::value<double>::serializer()(value, std::back_inserter(json)); 
std::cout << json << std::endl; 

json.clear(); 
wjson::value<double, 4>::serializer()(value, std::back_inserter(json)); 
std::cout << json << std::endl;


Результат:
1.234512 e+04 
12345.1234 

Зі строками, на перший погляд, повинно бути все просто, якщо ви використовуєте utf-8, але на наступні моменти потрібно звернути увагу:
  • серіалізація
    • всі utf-8 символи з кодами від 32 (пробіл) копіюються як є
    • символи '"','\', '/', '\t', '\b', '\r', '\n', '\f' екрануються '\' у відповідності зі специфікацією JSON

    • інші символи, з кодом меншим 32, сериализуются в шістнадцятковому форматі (\uXXXX)
    • не utf-8 серіалізуются побайтно у форматі \xXX, що не відповідає специфікації JSON, який працює виключно з utf-8, але десериализатор wjson цей формат розуміє
  • десериализация
    • екрановані символи деэкранируются
    • комбінації виду \uXXXX перетворено у utf-8, за винятком деяких значень менших 32 ( якщо XXXX не кодує '\t', '\b', '\r', '\n', '\f', то без перетворення)

    • комбінації виду \хХХ деэкранируются без перевірок
    • всі інші utf-8 символи копіюються як є


Деякі сторонні бібліотеки, особливо не напружуючись, сериализуют все, що не входить в діапазон ASCII (коди > 127) у форматі \uXXXX. Але при десеріалізації подібної рядки з допомогою wjson це декодується в utf-8. При повторній серіалізації wjson цього екранування вже не буде.

Іноді, як правило через програмної помилки, в середині рядка виявляється '\0', який більшістю сериализаторов, в тому числі і wjson, перетворюється в \u0000, але при десеріалізації він не перетворюється в 0, а залишається як є.

Підтримка формату \xXX продиктована виключно обмеженням концепції wjson-серіалізації, яка не передбачає валідних даних (або серіалізуются, або не функціонують). Для серіалізації бінарних даних використовуйте, наприклад, Base64.

Приклад серіалізації рядків
#include <wjson/json.hpp> 
#include < iostream> 
#include < cstring> 

int main() 
{ 
const char* english = "\"hello world!\""; 
const char* russian = "\"\\u041F\\u0440\\u0438\\u0432\\u0435\\u0442\\u0020\\u043C\\u0438\\u0440\\u0021\""; 
const char* chinese = "\"\\u4E16\\u754C\\u4F60\\u597D!\""; 

typedef char str_t[128]; 
typedef wjson::value< std::string, 128 >::serializer sser_t; 
typedef wjson::value< std::vector<char> >::serializer vser_t; 
typedef wjson::value< str_t >::serializer aser_t; 

std::string sstr; 
std::vector<char> vstr; 
str_t astr={'\0'}; 

// Десериализация 
sser_t()( sstr, english, english + std::strlen(english), 0); 
vser_t()( vstr, russian, russian + std::strlen(russian), 0); 
aser_t()( astr, chinese, chinese + std::strlen(chinese), 0); 

// Результат 
std::cout << "English: " << sstr << "\tfrom JSON: " << english << std::endl; 
std::cout << "Russian: " << std::string(vstr.begin(), vstr.end() ) << "\tfrom JSON: " << strike << std::endl; 
std::cout << "Chinese: " << astr << "\tfrom JSON: " << chinese << std::endl; 

// Серіалізація english в stdout 
std::cout << std::endl << "English JSON: "; 
sser_t()( sstr, std::ostream_iterator<char>( std::cout) ); 
std::cout << "\tfrom: " << sstr; 

// Серіалізація russian у stdout 
std::cout << std::endl << "Russian JSON: "; 
vser_t()( vstr, std::ostream_iterator<char>( std::cout) ); 
std::cout << "\tfrom: " << std::string(vstr.begin(), vstr.end() ); 

// Серіалізація chinese в stdout 
std::cout << std::endl << "Chinese JSON: "; 
aser_t()( astr, std::ostream_iterator<char>( std::cout) ); 
std::cout << "\tfrom: " << astr; 
std::cout << std::endl; 
}


Результат:
English: hello world! from JSON: "hello world!" 
 
Russian: Привіт світ! from JSON: "\u041F\u0440\u0438\u0432\u0435\u0442\u0020\u043C\u0438\u0440\u0021" 
 
Chinese: 世界你好! from JSON: "\u4E16\u754C\u4F60\u597D!" 
 

 
English JSON: "hello world!" from: hello world! 
 
Russian JSON: "Привіт світ!" from: Привіт світ! 
 
Chinese JSON: "世界你好!" from: 世界你好! 
 


Масиви

Для опису JSON масивів використовується схожа з wjson::value конструкція:
template < typename T, int R = -1> 
struct array 
{ 
typedef T target; 
typedef implementation_defined serializer; 
}; 

Тут T — сериализуемый контейнер, а R — розмір резерву для stl-контейнерів, які підтримують цей метод. Начебто все просто, але запис виду: wjson::array<std::vector < int>> не спрацює, оскільки ми не знаємо, яким чином сериализовывать елемент контейнера, в даному випадку int. Правильна запис буде виглядати так:
typedef wjson::array< std::vector< wjson::value<int> > > vint_json;

В якості параметра T передаємо потрібний нам контейнер, але замість типу елемента контейнера передаємо його JSON-опис. Підтримуються:
  • V[N]
  • std::vector
  • std::deque
  • std::array
  • std::list
  • std::set
  • std::multiset
  • std::unordered_set
  • std::unordered_multiset

Зрозуміло, максимальну продуктивність забезпечують перші чотири варіанти. Заповнення списків і асоціативних контейнерів занадто накладно саме по собі.
Приклад, для класичних сі-масивів:
typedef wjson::value<int> int_json; 
typedef int vint_t[3]; 
typedef wjson::array< int_json[3] > vint_json; 

Ну і, звичайно ж, підтримуються багатовимірні масиви (наприклад, вектори векторів тощо), як показано в прикладі:

Приклад для вектора векторів
#include <wjson/json.hpp> 
#include <wjson/strerror.hpp> 
#include < iostream> 

int main() 
{ 
// Одновимірний масив 
typedef wjson::value<int> int_json; 
typedef std::vector < int> vint_t; 
typedef wjson::array< std::vector<int_json> > vint_json; 

std::string json="[ 1,\t2,\r3,\n4, /*п'ять*/ 5 ]"; 
vint_t vint; 
vint_json::serializer()(vint, json.begin(), json.end(), NULL); 
json.clear(); 
vint_json::serializer()(vint, std::back_inserter(json)); 
std::cout << json << std::endl; 

// Двовимірний масив (вектор векторів ) 
typedef std::vector< vint_t > vvint_t; 
typedef wjson::array< std::vector<vint_json> > vvint_json; 
json="[ [], [1], [2, 3], [4, 5, 6] ]"; 
vvint_t vvint; 
vvint_json::serializer()(vvint, json.begin(), json.end(), NULL); 
json.clear(); 
vvint_json::serializer()(vvint, std::back_inserter(json)); 
std::cout << json << std::endl; 

// Тривимірний масив (вектор векторів із векторів) 
typedef std::vector< vvint_t > vvvint_t; 
typedef wjson::array< std::vector<vvint_json> > vvvint_json; 
json="[ [[]], [[1]], [[2], [3]], [[4], [5, 6] ] ]"; 
vvvint_t vvvint; 
vvvint_json::serializer()(vvvint, json.begin(), json.end(), NULL); 
json.clear(); 
vvvint_json::serializer()(vvvint, std::back_inserter(json)); 
std::cout << json << std::endl; 
}


Тут ми беремо JSON-рядок, десереализуем її в контейнер, очищаємо, сериализуем в цю ж рядок і виводимо:
[1,2,3,4,5] 
[[],[1],[2,3],[4,5,6]] 
[[[]],[[1]],[[2],[3]],[[4],[5,6]]] 

У рядку «json» показано, що між елементами масиву можуть бути будь-які пробільні символи, в тому числі переклад рядка, а також коментарі в сі-стилі, що дуже зручно при реалізації json-конфігурації.

Максимальний розмір для динамічних контейнерів не обмежений, а для сі-масивів і std::array обмеженням є власне розмір масиву. Якщо у вхідному JSON-елементів менше, то решту заповнюються значенням за замовчуванням, а якщо більше, то зайві просто відкидаються.

Якщо JSON-масиви містять елементи різних типівЯкщо JSON-масиви містять елементи різних типів, то вони сериализуются і десериализуются в два етапи. Спочатку потрібно описати контейнер рядків, які будуть містити довільні не десериализованные JSON-конструкції, наприклад:
typedef std::vector<std::string> vstr;

Для опису «сирого» JSON:
template < typename T = std::string, int R = -1> 
struct raw_value; 

Який копіює рядок JSON, як вона є, в контейнер T. А далі, з допомогою парсера, потрібно визначити тип JSON елемента і відповідним чином десериализовать його. У прикладі нижче ми намагаємося прочитати масив чисел [1,«2»,[3]] инкрементировать всі елементи і сериализации його, зберігаючи формат:

код
#include <wjson/json.hpp> 
#include <wjson/strerror.hpp> 
#include < iostream> 

int main() 
{ 
typedef std::vector< std::string > vect_t; 
typedef ::wjson::array< std::vector< ::wjson::raw_value<std::string> > > vect_json; 

vect_t inv; 
vect_t outv; 

std::string json = "[1,\"2\",[3]]"; 

std::cout << json << std::endl; 
vect_json::serializer()( inv, json.begin(), json.end(), 0 ); 
for ( auto& v : inv ) 
{ 
outv.push_back(""); 
if ( wjson::parser::is_number(v.begin(), v.end()) ) 
{ 
int num = 0; 
wjson::value<int>::serializer()( num, v.begin(), v.end(), 0); 
++num; 
wjson::value<int>::serializer()( num, std::back_inserter(outv.back()) ); 
} 
else if ( wjson::parser::is_string(v.begin(), v.end()) ) 
{ 
std::string snum; 
wjson::value<std::string>::serializer()( snum, v.begin(), v.end(), 0); 
int num = 0; 
wjson::value<int>::serializer()( num, snum.begin(), snum.end(), 0); 
++num; 
snum.clear(); 
wjson::value<int>::serializer()( num, std::back_inserter(snum) ); 
wjson::value<std::string>::serializer()( snum, std::back_inserter(outv.back()) ); 

} 
else if ( wjson::parser::is_array(v.begin(), v.end()) ) 
{ 
std::vector < int> vnum; 
wjson::array< std::vector< wjson::value<int> > >::serializer()( vnum, v.begin(), v.end(), 0); 
++vnum[0]; 
wjson::array< std::vector< wjson::value<int> > >::serializer()( vnum, std::back_inserter(outv.back()) ); 
} 
else 
{ 
outv.back()="null"; 
} 
} 

json.clear(); 
vect_json::serializer()( outv, std::back_inserter(json) ); 
std::cout << json << std::endl; 
}


Результат:
[1,"2",[3]] 
[2,"3",[4]] 

Це працює також і з об'єктами, і зі словниками, про яких мова піде далі. Якщо числа у вас можуть бути представлені тільки двома варіантами, рядком або, власне числом, то можна використовувати обгортку:
template < typename J, bool SerQ = true, bool ReqQ = true, int R = -1> 
struct quoted;

  • J — вихідне JSON опис
  • SerQ — попередньо сериализовывать в рядок
  • ReqQ — вхідний JSON повинен бути «рядком»
  • R резерв для проміжного буфера (рядки)

Насправді ця конструкція працює для будь-якого JSON-опису. Параметр SerQ включає подвійну серіалізацію. Наприклад, для чисел це означає просто обрамлення в лапки. Параметр ReqQ включає подвійну десеріалізацію, тобто він вимагає, щоб на вході була JSON-рядок. Якщо він вимкнений, то правила трохи складніше. Якщо на вході не JSON-рядок, то він просто запускає десериализатор J без попередньої десеріалізації. Якщо на вході JSON-рядок, то він десериализует у проміжний std::string. Якщо J описує не строкову сутність, то повторна десериализация з проміжного std::string. Для строкових сутностей визначаємо необхідність повторної десеріалізації. Це означає, що якщо після першої десеріалізації проміжна рядок починається з лапки, то це двічі сериализованная рядок і десериализируем ще раз, в іншому випадку просто копіюємо.

Зрозуміло, що wjson::quoted<> дає додатковий оверхед і її варто розглядати як тимчасовий милицю, на випадок, якщо з якихось причин клієнт почав «чудити» і сериализации числа термінами або робити подвійну серіалізацію вкладених об'єктів.


Парсер

У wjson є клас parser, що містить виключно static-методи, які можна розділити на два типи. Це перевірка на відповідність того або іншого JSON-типу і, відповідно, методи — парсери. Для кожного JSON-типу є свій метод:

список методів
class parser 
{ 
/*...*/ 
public: 
template < typename P> 
static P parse_space( P beg, P end, json_error* e); 

template < typename P> 
static P parse_null( P beg, P end, json_error* e ); 

template < typename P> 
static P parse_bool( P beg, P end, json_error* e ); 

template < typename P> 
static P parse_number( P beg, P end, json_error* e ); 

template < typename P> 
static P parse_string( P beg, P end, json_error* e ); 

template < typename P> 
static P parse_object( P beg, P end, json_error* e ); 

template < typename P> 
static P parse_array( P beg, P end, json_error* e ); 

template < typename P> 
static P parse_value( P beg, P end, json_error* e ); 
/*...*/ 
};


Так само, як і для десериализатора, тут beg — початок буфера, end-кінець буфера, а в «e», якщо не одно nullptr, буде записаний код помилки. У разі успіху, буде повернуто покажчик на символ, наступний за останнім символом поточної сутності. А в разі помилки, буде повернуто end, і ініціалізований e.

Припустимо, у вас є рядок з кількома JSON-об'єктами певної структури, які розділені переведенням рядка або іншими пробельными сутностями, тоді її можна відпрацювати так (без обробки помилок):
for (;beg!=end;) 
{ 
beg = wjson::parser::parse_space(beg, end, 0); 
beg=my_json::serializer()(dict, beg, end, 0); 
/ * .... */ 
} 

Всі сериализаторы припускають, що першим символом повинен бути символ десериализуемого об'єкта, інакше буде помилка. Але, як я вже і говорив, всередині об'єктів і масивів можуть бути пробільні символи, в тому числі і коментарі, які десериализатор парсити тим же parse_space. Приклад парсинга рядки з кількома JSON сутностями:
wjson::json_error e; 
for (;beg!=end;) 
{ 
beg = wjson::parser::parse_space(beg, end, &e); 
beg = wjson::parser::parse_value(beg, end, &e); 
if ( e ) abort(); 
} 

Тут parse_value перевіряє будь-яку JSON-сутність на валідність. Якщо на вході parse_space не пробільний символ, то він просто поверне beg. Він може повернути помилку, якщо, наприклад, виявлений незакритий коментар в сі-стилі, але додаткова перевірка тут надлишкова. Якщо на вхід парсеру (так само, як і десериализатору) приходить ініціалізований об'єкт помилки, то він просто повертає end.

Для визначення конкретної JSON-суті є наступний набір методів:

список методів
class parser 
{ 
/*...*/ 
public: 
template < typename P> 
static bool is_space( P beg, P end ); 

template < typename P> 
static bool is_null( P beg, P end ); 

template < typename P> 
static bool is_bool( P beg, P end ); 

template < typename P> 
static bool is_number( P beg, P end ); 

template < typename P> 
static bool is_string( P beg, P end ); 

template < typename P> 
static bool is_object( P beg, P end ); 

template < typename P> 
static bool is_array( P beg, P end ); 
}; 


Незважаючи на те, що вони отримують вказівники на початок і кінець буфера, ці методи визначають сутність першого символу: { — це об'єкт, [ — масив, “ — рядок, будь-яка цифра — це число, а t, f або n — це true, false або null відповідно. Тому, якщо, наприклад, is_object, нам повертає істину, то щоб переконатися, що це дійсний об'єкт, потрібно викликати parse_object і перевірити, чи немає помилок.

Обробка помилок

Перевіряти помилки при десеріалізації потрібно практично завжди. У прикладах я це не роблю виключно для наочності. Розглянемо на прикладі, де у вихідний масив впровадили сторонній символ:
#include <wjson/json.hpp> 
#include <wjson/strerror.hpp> 
#include < iostream> 

int main() 
{ 
typedef wjson::array< std::vector< wjson::value<int> > >::serializer serializer_t; 
std::vector < int > value; 
std::string json = "[1,2,3}5,6]"; 
wjson::json_error e; 
serializer_t()(value, json.begin(), json.end(), &e ); 
if ( e ) 
{ 
std::cout << "Error code: " << e.code() << std::endl; 
std::cout << "Error tail of: " << e.tail_of() << std::endl; 
if ( e.type() == wjson::error_code::ExpectedOf ) 
std::cout << "Error expected_of: " << e.expected_of() << std::endl; 

std::cout << "Error position: " 
<< wjson::strerror::where(e, json.begin(), json.end() ) << std::endl; 
std::cout << "Error message: " << wjson::strerror::message(e) << std::endl; 
std::cout << "Error trace: " 
<< wjson::strerror::trace(e, json.begin(), json.end()) << std::endl; 
std::cout << "Error message & trace: " 
<< wjson::strerror::message_trace(e, json.begin(), json.end()) 
<< std::endl; 
} 
} 

Власне, сам об'єкт помилки wjson::json_error містить інформацію про код помилки і позицію щодо кінця буфера, де парсер виявив будь-яку невідповідність. Для помилок спеціального типу «Expected of», символ, який він чекав.

Для отримання читабельних повідомлень використовуйте клас wjson::strerror. У прикладі вище в JSON-масиві зустрічається символ }, а парсер очікує кому (ну або квадратну дужку), про що він і повідомляє. У прикладі наведено всі доступні методи для аналізу помилки. Результат наступний:
Error code: 3 
Error tail of: 5 
Error expected_of: , 
Error position: 6 
Error message: Expected Of ',' 
Error trace: [1,2,3>>>}5,6] 
Error message & trace: Expected Of ',': [1,2,3>>>}5,6] 

Таким чином, можна отримати не лише код помилки, читабельне повідомлення, але і місце, де воно відбулося. При трасуванні використовується комбінація ">>>".

JSON Об'єкти
Десериализация JSON-об'єктів безпосередньо в структури даних — це те, заради чого і розроблявся wjson. Розглянемо просту структуру:
struct foo 
{ 
bool flag = false; 
int value = 0; 
std::string string; 
}; 

Яку потрібно сериализации в JSON типу:
{ "flag":true, "value":42, "string":"Привіт Світ!"} 

JSON-об'єкт — це просто перерахування списку полів, що складаються з імені та значення (будь JSON), розділених двокрапкою. Для серіалізації окремого поля потрібно скопіювати ім'я, яке відоме на етапі компіляції, додати двокрапка, і сериализации значення. Цю концепцію реалізує конструкція:
template < typename N, typename T, typename M, M T::* m, typename J = value<M> > 
struct member; 

  • N — ім'я поля
  • T — тип структури
  • М — тип поля
  • m — вказівник на поле структури
  • J — JSON-опис поля
Але явно передавати рядка параметрами шаблону проблематично. Тому скористаємося наступним трюком. Для кожного імені поля структури створимо конструкцію виду:

ім'я для flag
struct n_flag 
{ 
const char* operator()() const 
{ 
return "flag"; 
} 
}; 


Яку ми зможемо передавати параметром шаблону. Звичайно ж, плодити такі структури для кожного імені не дуже зручно, тому той рідкісний випадок, коли я дозволив собі макропідстановку. Для цього можна скористатися макросом:
JSON_NAME(flag)

який створить приблизно таку ж структуру. Префікс n_ використовуються з історичних причин. Але якщо він вам не подобається, можна використовувати другий варіант:
JSON_NAME2(n_flag, "flag")

який дозволяє створити структуру з довільним ім'ям і рядком. Приклад для опису окремого поля:
wjson::member< n_flag, foo, bool, &foo::flag>

Для простих типів JSON-опис ( wjson::value<> ) можна не передавати, але для всіх інших він потрібен. Сама по собі серіалізація поля структури не має особливого сенсу, тому потрібно об'єднати опису всіх полів в список наступним чином:
wjson::member_list< 
wjson::member<n_flag, foo, bool, &foo::flag>, 
wjson::member<n_value, foo, int, &foo::value>, 
wjson::member<n_string, foo, std::string &foo::string> 
> 

Для C++11 кількість полів не обмежена, для c++03 обмеження 26 елементів, яке легко обійти, використовуючи вкладені member_list. Правила серіалізації JSON-об'єкта структури дає конструкція:
template < typename T, typename L> 
struct object 
{ 
typedef T target; 
typedef implementation_defined serializer; 
typedef implementation_defined member_list; 
}; 

Тут T — тип структури даних, а L — список сериализуемых полів (member_list).

Приклад серіалізації та десеріалізації JSON-об'єкта
#include <wjson/json.hpp> 
#include <wjson/strerror.hpp> 
#include < iostream> 

struct foo 
{ 
bool flag = false; 
int value = 0; 
std::string string; 
}; 

JSON_NAME(flag) 
JSON_NAME(value) 
JSON_NAME(string) 

typedef wjson::object< 
foo, 
wjson::member_list< 
wjson::member<n_flag, foo,bool, &foo::flag>, 
wjson::member<n_value, foo,int, &foo::value>, 
wjson::member<n_string, foo,std::string &foo::string> 
> 
> foo_json; 

int main() 
{ 
std::string json="{\"flag\":false,\"value\":0,\"string\":\"Привіт Світ\"}"; 
foo f; 
foo_json::serializer()( f, json.begin(), json.end(), nullptr ); 

f.flag = true; 
f.string = "Поки Світ"; 
std::cout << json << std::endl; 
foo_json::serializer()( f, std::ostream_iterator<char>(std::cout) ); 
} 


Результат:
{"flag":false,"value":0,"string":"Привіт Світ"} 
{"flag":true,"value":0,"string":"Поки Світ"} 

На що хотілося б звернути вашу увагу:
  • у вихідній структурі (foo) немає жодної згадки про те, що вона є персистентной.
  • сериализуются поля рівно в тому порядку, як вони описані в member_list.
  • у вхідному JSON порядок полів не обов'язково повинен збігатися з порядком полів, описаному в member_list
  • описувати всі поля структури не обов'язково. Сериализуются тільки описані поля
  • всі інші поля з вхідного JSON ігноруються
  • member_list можна описати поля і базових класів в довільному порядку
  • спадкування підтримується, в тому числі і множинне (мається на увазі не віртуальне успадкування структур даних)
Якщо у вхідному JSON порядок полів збігається з порядком в JSON-описі, то десериализация відбувається максимально швидко, по суті, за один прохід. Пропуски полів або зайві елементи на продуктивність не сильно впливають (вони просто ігноруються).

Але що буде, якщо поля в JSON прийшли в довільному порядку? Зрозуміло, це позначається на продуктивності, тому що парсер збивається і починає перебір полів спочатку. Але я рекомендую взагалі не морочитися темою порядку полів.

Ще до того моменту, коли це почне реально відчуватися, ви зіткнетеся з проблемою часу не десеріалізації, а надмірності JSON, і потрібно буде думати про зміну формату обміну даними. Це не обов'язково означає перехід на бінарні протоколи. Можна, наприклад, передавати об'єкти у вигляді JSON-масивів, в якому позиція жорстко відповідає деякому полю структури. В окремих випадках, коли передається багато нулів, такий формат може бути і компактніше, і швидше protobuf.

Щоб не бути голослівним, я прогнав на продуктивність десеріалізацію наступної структури:

struct foo 
{ 
int field1 = 0; 
int field2 = 0; 
int field3 = 0; 
std::vector < int> field5; 
};

JSON опис foo
JSON_NAME(field1) 
JSON_NAME(field2) 
JSON_NAME(field3) 
JSON_NAME(field5) 

typedef wjson::object< 
foo, 
wjson::member_list< 
wjson::member<n_field1, foo, int, &foo::field1>, 
wjson::member<n_field2, foo, int, &foo::field2>, 
wjson::member<n_field3, foo, int, &foo::field3>, 
wjson::member<n_field5, foo, std::vector < int>, &foo::field5, ::wjson::array< std::vector< ::wjson::value<int> > > > 
> 
> foo_json; 


З прямої та зворотної (невдалої) послідовностями полів у вхідному JSON. Але потім помітив, що імена полів підібрані не зовсім чесно, т. к. збігаються за винятком останнього символу, тому також зробив замір для варіанту:
JSON_NAME2(n_field1, "1field") 
JSON_NAME2(n_field2, "2field") 
JSON_NAME2(n_field3, "3field") 
JSON_NAME2(n_field5, "5field") 

коли всі поля розрізняються першим символом. У підсумку для JSON:
{"field1":12345,"field2":23456,"field3":34567,"field5":[45678,56789,67890,78901,89012]} 
{"5field":[45678,56789,67890,78901,89012],"1field":12345,"2field":23456,"3field":34567} 
{"field5":[45678,56789,67890,78901,89012],"field1":12345,"field2":23456,"field3":34567} 

Отримав наступні результати:
  • Час серіалізації: 151321 ns (6608468 persec), зараз не важливо
  • Десериализация для «оптимального» JSON: 204113 ns (4899246 persec)
  • «Найгірший» порядок полів з оптимальними іменами: 221140 ns (4522022 persec)
  • «Найгірший» порядок полів з «поганими» іменами: 237616 ns (4208470 persec)

Для наочності і щоб закрити тему sprintf/sscanf, піднятою на початку статті, я також заміряв час виконання такої конструкції:
sscanf( str, "{\"field1\":%d,\"field2\":%d,\"field3\":%d,\"field5\":[%d,%d,%d,%d,%d]}", 
&(f.field1), &(f.field2), &(f.field3), &(f.field5[0]), &(f.field5[1]), &(f.field5[2]),&(f.field5[3]), &(f.field5[4]) ); 

Зрозуміло, що тут і мови не може бути про повноцінної десеріалізації — будь-яка невідповідність паттерну може привести до плачевних результатів. Тим не менш, результат 2477942 ns (403560 persec), що в десять разів гірше, ніж у wjson зі всіма перевірками, з «поганим» порядком та «не вдалими» іменами полів:

Для тих, хто не повірив своїм очам, і хоче перевірити ці цифри (що похвально), не дочитавши статті (а це мені складно вітати), відразу попереджу, що це працює тільки з включеною оптимізацією. У дебаг режимі отримаєте цифри з точністю до навпаки і навіть гірше. Завжди треба чимось жертвувати.

Питання в плані швидкості серіалізації подібних сутностей для мене вже стоїть кілька років, але проблема надмірності JSON іноді спливає. Для вирішення цієї проблеми є можливість серіалізації структури в масив, у якого поля жорстко прив'язані до індексу:
typedef wjson::object_array< 
foo, 
wjson::member_list< 
wjson::member_array<foo, int, &foo::field1>, 
wjson::member_array<foo, int, &foo::field2>, 
wjson::member_array<foo, int, &foo::field3>, 
wjson::member_array<foo, std::vector < int>, &foo::field5, ::wjson::array< std::vector< ::wjson::value<int> > > > 
> 
> foo_json; 

В результаті та ж структура серіалізуются в масив:
[12345,23456,34567,[45678,56789,67890,78901,89012]] 

за 139856 ns (7150211 persec), а десериализация відбувається за 131282 ns (7617190)

Так, різниця у швидкості є, але в першу чергу — це компактніше. В одному з проектів, в якому сервер повертає дані для побудови графіків, де кожна точка описана вісьмома полями, яких близько 3000 для кожного графіка, а самих графіків на екрані може бути декілька десятків, результуючий JSON міг бути в декілька мегабайт. Сериализовывая вихідні структури в масиви з восьми елементів, ми не тільки суттєво скорочуємо обсяг переданого трафіку, але і збільшуємо його читабельність. Але, загалом, звичайно ж, переводити все API на масиви не найкраща ідея.

Спадкування



Розглянемо успадкування на прикладі наступних структур:
struct foo 
{ 
bool flag = false; 
int value = 0; 
std::string string; 
}; 

struct bar: foo 
{ 
std::vector < int> data; 
}; 

Є два способи описати спадкування.Варіант перший:
typedef ::wjson::array< std::vector< ::wjson::value<int> > > vint_json; 
typedef wjson::object< 
bar, 
wjson::member_list< 
wjson::member<n_flag, foo,bool, &foo::flag>, 
wjson::member<n_value, foo,int, &foo::value>, 
wjson::member<n_string, foo,std::string &foo::string>, 
wjson::member<n_data, bar, std::vector < int>, &bar::data, vint_json> 
> 
> bar_json; 

Тут ми можемо розташовувати поля батьків (або батьків) і спадкоємця в будь-якому порядку. Варіант другий, більш наочний:
typedef wjson::object< 
foo, 
wjson::member_list< 
wjson::member<n_flag, foo,bool, &foo::flag>, 
wjson::member<n_value, foo,int, &foo::value>, 
wjson::member<n_string, foo,std::string &foo::string> 
> 
> foo_json; 

typedef wjson::object< 
bar, 
wjson::member_list< 
wjson::base<foo_json>, 
wjson::member<n_data, bar, std::vector < int>, &bar::data, vint_json> 
> 
> bar_json; 

Робимо окреме JSON-опис базового класу і впроваджуємо з допомогою конструкції wjson::base<foo_json>, яка є псевдонімом для foo_json::member_list, в будь-яке місце у списку.

Великий приклад, з усіма елементами
#include <wjson/json.hpp> 
#include <wjson/strerror.hpp> 
#include < iostream> 

struct foo 
{ 
bool flag = false; 
int value = 0; 
std::string string; 
}; 

struct bar: foo 
{ 
std::shared_ptr<foo> pfoo; 
std::vector<foo> vfoo; 
}; 

struct foo_json 
{ 
JSON_NAME(flag) 
JSON_NAME(value) 
JSON_NAME(string) 

typedef wjson::object< 
foo, 
wjson::member_list< 
wjson::member<n_flag, foo,bool, &foo::flag>, 
wjson::member<n_value, foo,int, &foo::value>, 
wjson::member<n_string, foo,std::string &foo::string> 
> 
> type; 
typedef type::serializer serializer; 
typedef type::target target; 
typedef type::member_list member_list; 
}; 

struct bar_json 
{ 
JSON_NAME(pfoo) 
JSON_NAME(vfoo) 
typedef wjson::array< std::vector< foo_json > > vfoo_json; 
typedef wjson::pointer< std::shared_ptr<foo>, foo_json > pfoo_json; 

typedef wjson::object< 
bar, 
wjson::member_list< 
wjson::base<foo_json>, 
wjson::member<n_vfoo, bar, std::vector<foo>, &bar::vfoo, vfoo_json>, 
wjson::member<n_pfoo, bar, std::shared_ptr<foo>, &bar::pfoo, pfoo_json> 
> 
> type; 
typedef type::serializer serializer; 
typedef type::target target; 
typedef type::member_list member_list; 
}; 

int main() 
{ 
std::string json="{\"flag\":true,\"value\":0,\"string\":\"Привіт Світ\",\"vfoo\":[],\"pfoo\":null}"; 
bar b; 
bar_json::serializer()( b, json.begin(), json.end(), nullptr ); 
b.flag = true; 
b.vfoo.push_back( static_cast<const foo&>(b)); 
b.pfoo = std::make_shared<foo>(static_cast<const foo&>(b)); 
std::cout << json << std::endl; 
bar_json::serializer()(b, std::ostream_iterator<char>(std::cout) ); 
std::cout << std::endl; 
} 


Результат:
{"flag":true,"value":0,"string":"Привіт Світ","vfoo":[],"pfoo":null} 
{"flag":true,"value":0,"string":"Привіт Світ","vfoo":[{"flag":true,"value":0,"string":"Привіт Світ"}],"pfoo":{"flag":true,"value":0,"string":"Привіт Світ"}} 

Тут трохи інше опис JSON-об'єктів, але спочатку про покажчики. Сериализовывать можна будь-які дороговкази. Якщо він дорівнює нулю, то і серіалізуются як null, в іншому разі за значенням. А десериализация реалізована тільки для std::shared_ptr<>. Якщо в JSON null, — це nullptr, в іншому випадку створюється об'єкт і у нього відбувається десериализация. Для будь-яких елементів, які ми не описали як wjson::pointer, якщо на вхід приходить null, то він створюється зі значенням за замовчуванням. Це відноситься також до масивів і простим типами.
Навіщо екранувати шаблонні класи з великим числом параметрівЕкрануючи JSON-опису структурами, як показано в прикладі, ми вбиваємо відразу декілька зайців. Наводимо порядок з іменами полів і їх конфліктами, скорочуємо час компіляції, зменшуємо розмір бінарників і спрощуємо висновок помилок компілятора. Припустимо, є така от конструкція:
template < typename J> 
struct deserealizer 
{ 
typedef typename J::deserializer type; 
}; 

Яку ми намагаємося використовувати наступним чином:
typedef deserealizer<bar_json>::type deser; 

Т. к. тип deserializer у нас ніде не визначено, то отримаємо помилку:
error: no type named 'deserializer' in 'struct bar_json' 

Якщо foo_json і bar_json визначені через typedef, то:
error: no type named 'deserializer' in 'struct wjson::object<bar, fas::type_list<wjson::member<n_flag, foo, bool, &foo::flag>, fas::type_list<wjson::member<n_value, foo, int, &foo::value>, fas::type_list<wjson::member<n_string, foo, std::basic_string<char>, &foo::string>, fas::type_list<wjson::member<n_vfoo, bar, std::vector<foo>, &bar::vfoo, wjson::array<std::vector<wjson::object<foo, fas::type_list<wjson::member<n_flag, foo, bool, &foo::flag>, fas::type_list<wjson::member<n_value, foo, int, &foo::value>, fas::type_list<wjson::member<n_string, foo, std::basic_string<char>, &foo::string>, fas::empty_list> > > > > > >, fas::type_list<wjson::member<n_pfoo, bar, std::shared_ptr<foo>, &bar::pfoo, wjson::pointer<std::shared_ptr<foo>, wjson::object<foo, fas::type_list<wjson::member<n_flag, foo, bool, &foo::flag>, fas::type_list<wjson::member<n_value, foo, int, &foo::value>, fas::type_list<wjson::member<n_string, foo, std::basic_string<char>, &foo::string>, fas::empty_list> > > > > >, fas::empty_list> > > > > >' 

Як кажуть, відчуйте різницю. Цей прийом дозволяє прискорити час компіляції, незважаючи на те, що компілятор буде потрібно инстанцировать зайву сутність.Нагадаю, що компілятор при инстансировании шаблонного класу включає в його ім'я всі шаблонні параметри, які можна побачити через typeid(T).name(). І, в ряді випадків, вигідніше змусити його зробити проміжний інстанси, з коротким іменем, з яким йому буде легше працювати. Але ні в якому разі не можна застосовувати цей прийом всередині шаблонних конструкцій, наприклад, так:
template < typename J> 
struct deserealizer 
{ 
struct type: J::deserializer {}; 
}; 

У ряді випадків це буде працювати, але насправді тільки погіршить ситуацію. Загальне правило можна сформулювати так. Шаблонні сутності верхнього рівня з великою кількістю параметрів, які ви передаєте в тому чи іншому вигляді в якості шаблонних параметрів інших сутностей, має сенс екранувати, якщо вони дозволяють (не використовують спеціалізації).

Ну і, зрозуміло, ніхто не заважає зробити той же foo_json шаблонним і передати параметром, наприклад, тип поля value, що можна використовувати для серіалізації шаблонних структур.

Словники


Словники потрібні для серіалізації асоціативних масивів (ключ-значення), наприклад, std::map<>. За цією схемою працюють більшість JSON бібліотек — об'єкт десериализуется в дерево, а далі ви його досліджуєте, здійснюєте пошук потрібних полів і т. д. А для серіалізації вам його необхідно динамічно заповнити. З точки зору продуктивності не найефективніший метод. Тому перш ніж використовувати той std::map<> в структурах даних, подумайте, а чи можна якось без нього. Зрозуміло, це не відноситься до випадку, якщо ви використовуєте JSON для конфігурації:

template < typename T, int R = -1> 
struct dict 
{ 
typedef implementation_defined target; 
typedef implementation_defined serializer; 
}; 

Тут концепція та ж, що і в масивів — T це асоціативний stl контейнер, в якості параметрів якому задані JSON опису для ключа і значення. Наприклад:
typedef wjson::dict< 
std::map< 
wjson::value<std::string>, 
wjson::value<int> 
> 
> dict_json; 

може бути використаний для серіалізації std::map<std::string, int>. Конструкція досить складна, але з урахуванням того, що найчастіше в якості ключа використовуються рядки, є більш простий варіант для std::map<std::string, JSON>:
typedef wjson::dict_map< wjson::value<int> > dict_json; 

Зрозуміло, значення може бути використана будь-яка JSON сутність, описана вище в цій статті.

Параметр R визначає розмір резерву, для послідовних контейнерів пар типу std::vector< std::pair<> > або std::deque< std::pair<> >. Для опису пари ключ-значення використовується конструкція field:
template < typename K, typename V> 
struct field; 

Тут і До V JSON-опису ключа і значення, відповідно. Наприклад:
typedef wjson::dict< 
std::vector< 
wjson::field< 
wjson::value<std::string>, 
wjson::value<int> 
> 
>, 
128 /*резерв при десеріалізації*/ 
> dict_json; 

для серіалізації вектора пар std::vector< std::pair<std::string, int> >. Цю конструкцію можна використовувати там, де потрібна висока швидкість десеріалізації. Вектор пар заповнюється істотно швидше, ніж std::map (зрозуміло, якщо був зроблений необхідний резерв). Ця конструкція ще складніше, а використовується частіше, тому для неї є простий варіант:
typedef wjson::dict_vector< ::wjson::value<int> > dict_json; 
typedef wjson::dict_deque< ::wjson::value<int> > dict_json; 


Наприклад:
int main() 
{ 
typedef std::vector< std::pair<std::string, int> > dict; 
typedef wjson::dict_vector< wjson::value<int> > dict_json; 

dict d; 
std::string json = "{\"один\":1,\"два\":2,\"три\":3}"; 
std::cout << json << std::endl; 
dict_json::serializer()( d, json.begin(), json.end(), 0 ); 
d.push_back( std::make_pair("чотири",4)); 
json.clear(); 
dict_json::serializer()( d, std::back_inserter(json) ); 
std::cout << json << std::endl; 
} 

Результат:
{"":1,"два":2,"три":3} 
{"":1,"два":2,"три":3,"чотири":4} 

Словники зручно використовувати для конфігурацій. У найпростішому випадку — це простий масив ключ-значення, де у складі ключа буде присутній ім'я конфигурируемого компонента. Але якщо не полінуватися, винести конфігурацію компонента в окрему структуру і зробити для нього JSON-опис, то ви позбавитеся від зайвої runtime коду, а, отже, і від помилок-описок ініціалізації. Крім того, ви зможете генерувати конфігурацію з актуальних набором полів на радість собі (на етапі розробки) і користувачеві, щоб він міг переконатися в актуальності документації, яка має властивість застарівати.

Перерахування і прапори



Є сенс сериализовывать перерахування в їх текстовому поданні з тієї ж причини, навіщо були створені текстові формати представлення даних взагалі — це читабельність. Якщо не зловживати довгими іменами, то може вийти, як мінімум, не повільніше серіалізації у вигляді числа і не дуже накладно по розміру. Якщо ви дочитали до цих рядків, то, швидше за все, загальна концепція в цілому зрозуміла, тому відразу наприклад:

Приклад серіалізації enum
#include <wjson/json.hpp> 
#include <wjson/strerror.hpp> 
#include < iostream> 

struct counter 
{ 
typedef enum 
{ 
one = 1, 
four = 4, 
five = 5, 
two = 2, 
three = 3, 
six = 6 
} type; 
}; 

struct counter_json 
{ 
JSON_NAME(one) 
JSON_NAME(two) 
JSON_NAME(three) 
JSON_NAME(four) 
JSON_NAME(five) 
JSON_NAME2(n_six, "Привіт світ!") 

typedef wjson::enumerator< 
counter::type, 
wjson::member_list< 
wjson::enum_value< n_one, counter::type, counter::one>, 
wjson::enum_value< n_two, counter::type, counter::two>, 
wjson::enum_value< n_three,counter::type, counter::three>, 
wjson::enum_value< n_four, counter::type, counter::four>, 
wjson::enum_value< n_five, counter::type, counter::five>, 
wjson::enum_value< n_six, counter::type, counter::six> 
> 
> type; 

typedef type::serializer serializer; 
typedef type::target target; 
typedef type::member_list member_list; 
}; 

int main() 
{ 
typedef wjson::array< std::vector< counter_json > > array_counter_json; 

std::vector< counter::type > cl; 
std::string json = "[\"one\",\"two\",\"three\"]"; 
std::cout << json << std::endl; 

array_counter_json::serializer()( cl, json.begin(), json.end(), 0 ); 
cl.push_back(counter::four); 
cl.push_back(counter::five); 
cl.push_back(counter::six); 

array_counter_json::serializer()(cl, std::ostream_iterator<char>(std::cout) ); 
std::cout << std::endl; 
} 


Як показано в прикладі, перерахування зовсім не обов'язково сериализовывать один в один, а можна в довільну рядок. Результат:
["one","two","three"] 
["one","two","three","four","five","Привіт світ!"] 

Насправді перерахування тут просто для зручності, і можна використовувати будь-які цілочисельні типи.

Розглянемо ось такий JSON:
{"code":1,"message":"Invalid JSON."} 

Це якесь повідомлення про помилку в стилі JSON-RPC. Очевидно, що повідомлення message безпосередньо пов'язано з кодом. Тому немає необхідності створювати структуру з текстовим полем і заповнювати її, достатньо:

Два в одному і навіть не enum

#include <wjson/json.hpp> 
#include <wjson/strerror.hpp> 
#include < iostream> 

enum class error_code 
{ 
ValidJSON = 0, 
InvalidJSON = 1, 
ParseError = 2 
}; 

struct error 
{ 
int code = 0; 
}; 

struct code_json 
{ 
JSON_NAME2(ValidJSON, "Valid JSON.") 
JSON_NAME2(InvalidJSON, "Invalid JSON.") 
JSON_NAME2(ParseError, "Parse Error.") 

typedef wjson::enumerator< 
int, 
wjson::member_list< 
wjson::enum_value< ValidJSON, int, static_cast<int>(error_code::ValidJSON)>, 
wjson::enum_value< InvalidJSON, int, static_cast<int>(error_code::InvalidJSON)>, 
wjson::enum_value< ParseError, int, static_cast<int>(error_code::ParseError)> 
> 
> type; 

typedef type::serializer serializer; 
typedef type::target target; 
typedef type::member_list member_list; 
}; 

struct error_json 
{ 
JSON_NAME(code) 
JSON_NAME(message) 

typedef wjson::object< 
error, 
wjson::member_list< 
wjson::member< n_code, error, int, &error::code>, 
wjson::member< n_message, error, int, &error::code, code_json> 
> 
> type; 

typedef type::serializer serializer; 
typedef type::target target; 
typedef type::member_list member_list; 
}; 

int main() 
{ 
error e; 
e.code = static_cast<int>(error_code::InvalidJSON); 
error_json::serializer()(e, std::ostream_iterator<char>(std::cout) ); 
std::cout << std::endl; 
} 


Результат:
{"code":1,"message":"Invalid JSON."} 

Серіалізація виглядає досить забавно, але як бути з десериализацией? Виходить, що «code» десериализуется два рази: один раз з поля «code», а другий раз з поля «message». Можна зробити окремий варіант error_json спеціально для десеріалізації без поля «message», але на продуктивності це не сильно позначиться, так як при серіалізації воно все одно парс. А можна використовувати цю особливість для подвійної перевірки, щоб код суворо відповідав. Наприклад, якщо замінити в повідомленні точку на знак питання:
e = error(); 
std::string json = "{\"code\":1,\"message\":\"Invalid JSON?\"}"; 
wjson::json_error ec; 
error_json::serializer()(e, json.begin(), json.end(), &ec ); 

То отримаємо помилку:
Invalid Enum: {"code":1,"message":">>>Invalid JSON?"} 

Часто перерахування використовуються для всіляких прапорцевих комбінацій, їх теж можна сериализации. Але в якому вигляді? Пропонуються два способи: у вигляді масиву або у вигляді рядка з заданим роздільником. Разделитель задається останнім параметром flags. Будь-який символ, крім коми, буде сериализовывать в рядок, а для комою — в масив. У жартівливому прикладі нижче використані обидва варіанти:

жив-був у бабусі сіренький козлик

#include <wjson/json.hpp> 
#include <wjson/strerror.hpp> 
#include < iostream> 

template<char S> 
struct flags_json 
{ 
JSON_NAME2(w1, "жив") 
JSON_NAME2(w2, "") 
JSON_NAME2(w4, "у") 
JSON_NAME2(w8, "бабусі") 
JSON_NAME2(w16, "сіренький") 
JSON_NAME2(w32, "козлик") 

typedef ::wjson::flags< 
int, 
wjson::member_list< 
wjson::enum_value< w1, int, 1>, 
wjson::enum_value< w2, int, 2>, 
wjson::enum_value< w4, int, 4>, 
wjson::enum_value< w8, int, 8>, 
wjson::enum_value< w16, int, 16>, 
wjson::enum_value< w32, int, 32> 
>, 
S 
> type; 

typedef typename type::serializer serializer; 
typedef typename type::target target; 
typedef typename type::member_list member_list; 
}; 

int main() 
{ 
std::string json = "\"жив був сіренький козлик\""; 
int val = 0; 
flags_json<' '>::serializer()(val, json.begin(), json.end(), 0 ); 
std::cout << json << " = " << val << std::endl; 

std::cout << 63 << " = "; 
flags_json<' '>::serializer()(63, std::ostream_iterator<char>(std::cout) ); 
std::cout << std::endl; 

std::cout << 48 << " = "; 
flags_json<','>::serializer()(48, std::ostream_iterator<char>(std::cout) ); 
std::cout << std::endl; 

std::cout << 49 << " = "; 
flags_json<'|'>::serializer()(49, std::ostream_iterator<char>(std::cout) ); 
std::cout << std::endl; 
} 


Ідея тут проста. Кожне слово з рядка дитячої пісеньки ми використовуємо як прапор з відповідним значенням. Комбінуючи прапори, отримуємо різні варіанти. А якщо в якості роздільника використовуємо пробіл, то взагалі не очевидно, що це набір прапорів. Спочатку ми десериализуем рядок «жив був сіренький козлик», що відповідає комбінації 1/2/16/32=51, з роздільником як пробілу. А далі показані приклади серіалізації з різними роздільниками. Очевидно, що можна використовувати всі числа аж до 63 — це фраза цілком.
Результат:
"жив був сіренький козлик" = 51 
63 = "жив-був у бабусі сіренький козлик" 
48 = ["сіренький","козлик"] 
49 = "жив|сіренький|козлик" 


Висновок


Досить важко не піддатися спокусі, аби не почати довге і нудне виправдувальне розповідь про історію розробки цього творіння. Тому коротко. Написана на коліні в 2008 просто для відпрацювання деяких концепцій faslib, на яких вона побудована. У 2009 wjson (тоді це був просто набір коду який копипастился) використовувалася в експериментальних проектах. Тоді ж стало ясно, що інтерфейс не гнучкий і взагалі повний відстій. У 2011 році була спроба зробити щось глобальне, всеосяжне і правильне. І це майже вийшло, але було закинуто, т. к. в тому ж році ми стали переводити на JSON всі наші проекти, і виявилося, що поточні можливості перекривають всі наші потреби, а інтерфейс простий і зрозумілий навіть новачкам. З 2013 всі наші проекти, в тому числі і дуже високонавантажених, працюють з wjson. Наприклад, comet-демон може підтримувати до 1 млн. одночасних активних підключень, а система збору статистики перемелює на одному хості більше 1.5 ГБ JSON-RPC повідомлень, реєструючи до 4.5 млн. значень різних метрик в секунду.

Ми використовуємо JSON для конфігурацій, всіляких дампів і, звичайно-ж, спільно JSON-RPC движком, який працює приблизно за таким же принципом, і про який я розповім в наступній статті зовсім скоро.

І wjson і faslib, від якої залежить wjson — це header-only бібліотеки. Для компілювання прикладів і тестів:
git clone https://github.com/migashko/faslib.git 
git clone https://github.com/mambaru/wjson.git 

# потрібно тільки для компіляції тестів wjson 
cd faslib 
mkdir build 
cd build 
cmake .. 

# збираємо приклади і тести 
cd ../../wjson 
mkdir build 
cd build 
cmake -DWJSON_BUILD_ALL=ON .. 
make 
cd ./tests 
ctest 

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.