Отображение данных в формате json на структуру C++

ПрограммированиеC++
Из песочницы

В идеале хотелось бы определить структуру С++


struct Person {
 std::string name;
 int age;
 bool student;
} person;

передать экземпляр person в метод отображения вместе с данными json_data


map_json_to_struct(person, json_data)

после чего просто пользоваться заполненной структурой


std::cout << person.name << " : " << person.age;

StructMapping пытается решить эту задачу.


UPD На данный момент библиотека существенно обновлена, и все, показанное здесь, (кроме основной идеи) уже не актуально. Все изменения отражены во второй статье Отображение данных в формате json на структуру c++ и обратно (работа над ошибками)


Для обеспечения сценария, максимально приближенного к приведенному выше, StructMapping требует от разработчика определить особым образом поля структуры.


Реализация сценария со структурой Person


#define MANAGED_STRUCT_NAME Person // определяем макрос, который задает имя
                                   // структуры
BEGIN_MANAGED_STRUCT               // определяем начало структуры

MANAGED_FIELD(std::string, name)   // определяем поле с типом 'std::string'и именем
                                   // 'name'
MANAGED_FIELD(int, age)            // определяем поле с типом 'int' и именем 'age'
MANAGED_FIELD(bool, student)       // определяем поле с типом 'bool' и именем
                                   // 'student'

END_MANAGED_STRUCT                 // определяем конец структуры
#undef MANAGED_STRUCT_NAME         // убираем макрос, который задавал имя структуры,
                                   // чтобы не было варнингов о переопределении
                                   // макроса в дальнейшем

создаем экземпляр


Person person;

задаем json данные


std::istringstream json_data(R"json(
{
 "name": "Jeebs",
 "age": 42,
 "student": true
}
)json");

передаем экземпляр person в метод отображения вместе с данными json


struct_mapping::mapper::map_json_to_struct(person, json_data);

пользуемся


std::cout << person.name << " : " << person.age;

Полностью код выглядит так
#include <iostream>
#include <sstream>

#include "struct_mapping/struct_mapping.h"

#define MANAGED_STRUCT_NAME Person
BEGIN_MANAGED_STRUCT

MANAGED_FIELD(std::string, name)
MANAGED_FIELD(int, age)
MANAGED_FIELD(bool, student)

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

int main() {
 Person person;

 std::istringstream json_data(R"json(
  {
   "name": "Jeebs",
   "age": 42,
   "student": true
  }
 )json");

 struct_mapping::mapper::map_json_to_struct(person, json_data);

 std::cout <<
  person.name << " : " <<
  person.age << " : " <<
  std::boolalpha << person.student <<
  std::endl;
}

Дополнительные типы полей


Кроме простых типов (логического типа, целочисленные, с плавающей точкой и строки) поле структуры может быть так же структурой


MANAGED_FIELD_STRUCT(тип поля, имя поля)

Например так
#define MANAGED_STRUCT_NAME President <-- определяем структуру President
BEGIN_MANAGED_STRUCT

MANAGED_FIELD(std::string, name)
MANAGED_FIELD(double, mass)

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

#define MANAGED_STRUCT_NAME Earth
BEGIN_MANAGED_STRUCT

MANAGED_FIELD_STRUCT(President, president) <-- определяем поле с типом President

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

или массивом


MANAGED_FIELD_ARRAY(тип элемента массива, имя поля)

размерность массивов можно увеличивать


MANAGED_FIELD_ARRAY(MANAGED_ARRAY(MANAGED_ARRAY(std::string)), planet_groups)

Пример определения структуры с массивами
#define MANAGED_STRUCT_NAME MiB
BEGIN_MANAGED_STRUCT

MANAGED_FIELD_ARRAY(std::string, friends)
MANAGED_FIELD_ARRAY(MANAGED_ARRAY(std::string), alien_groups)
MANAGED_FIELD_ARRAY(MANAGED_ARRAY(MANAGED_ARRAY(std::string)), planet_groups)

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

Как это работает


Основная задача — отобразить данные в формате json на структуру с++. Структура с++ — это набор полей. Каждое поле имеет имя и содержит значение определенного типа. Поэтому на структуру с++ отображаются json объекты. В структуре с++ типы полей соответствуют типам json значений и могут быть следующими:


  • bool — хранит json значение true или false
  • integral или floating point — хранит json число
  • std::string — хранит json строку
  • управляемая структура — хранит json объект
  • управляемый массив — хранит json массив

Для решения задачи используются парсер json и управляемые структуры.


Парсер json


При создании экземпляра парсера ему в конструкторе передается несколько функций, которые парсер будет вызывать процессе работы:


функция назначение
set_bool для установки логического значения
set_integral для установки целочисленного значения
set_floating_point для установки значения с плавающей точкой
set_string для установки строкового значения
start_struct для начала json объекта
end_struct для конца json объекта
start_array для начала json массива
end_array для конца json массива

Например для json данных


 {
  "price": 273,
  "author": {
   "name": "bk192077"
  },
  "chapters": [
   "launch",
   "new horizons"
  ]
 }

будут выполнена следующая последовательность вызовов


start_struct("")
set_integral("price", 273)
start_struct("author")
set_string("name", "bk192077")
end_struct()
start_array("chapters")
set_string("", "launch")
set_string("", "new horizons")
end_array()
end_struct()

Управляемые структуры


Общими словами можно сказать, что каждая управляемя структура имеет переменную (use_name), хранящую имя используемого поля. Если use_name не пустая, то она хранит имя поля, которому будут транслироваться события от парсера. Изначально use_name пустая (используемых полей нет).


  • если при парсинге встречается начало json объекта или json массива, то:
    • если use_name пустая, то в нее помещается имя поля, парсинг которого был начат
    • если use_name не пустая, то событие транслируется полю, имя которого содержится в use_name
  • если при парсинге встречается конец json объекта или json массива, то:
    • если use_name пустая, то ничего не происходит
    • если use_name не пустая, то событие транслируется полю, имя которого содержится в use_name. Если после трансляции события получен признак завершения цепочки использования, то use_name очищается
  • если при парсинге встречается установка значения, то:
    • если use_name пустая, то значение устанавливается для поля текущего экземпляра структуры по имени этого поля (а в случае массива значение добавляется в массив)
    • если use_name не пустая, то событие транслируется полю, имя которого содержится в use_name

Для этого управляемые структуры содержат несколько служебных функций


void set(std::string const &, bool) {...}
void set(std::string const &, long long) {...}
void set(std::string const &, double) {...}
void set(std::string const &, std::string const &) {...}
void use(std::string const &) {...}
bool release() {...}

Фактически эти функции вызываются парсером в следующем соответствии


парсер управляемая структура
set_bool set(std::string const &, bool)
set_integral set(std::string const &, long long)
set_floating_point set(std::string const &, double)
set_string set(std::string const &, std::string const &)
start_struct use(std::string const &)
end_struct release()
start_array use(std::string const &)
end_array release()

Например, для Person


функции set (почти одинаковы по реализации и перегружены по типу устанавливаемого значения)


void set(std::string const & field_name, bool value) {
 if (use_name.empty()) {
  // установка значения поля непосредственно у данного экземпляра структуры
  Fs_set_field<std::function<void(Person&, bool)>>::fs[field_name](*this, value);
 } else {
  // трансляция вызова полю, которое отмечено как используемое
  Fs_set<std::function<void(Person&, std::string const &, bool)>>
   ::fs[use_name](*this, field_name, value);
 }
}

функция use (после этого все вызовы будут транслироваться полю field_name)


void use(std::string const & field_name) {
 if (use_name.empty()) {
  // поле field_name становится используемым
  use_name = field_name;
 } else {
  // вызов транслируется полю use_name
  Fs_use<std::function<void(Person&, std::string const &)>>
   ::fs[use_name](*this, field_name);
 }
}

функция release (из цепочки использования удаляется последний элемент)


bool release() {
 // это была последняя структура в цепочке использования
 if (use_name.empty()) return true;

 if (Fs_release<std::function<bool(Person&)>>::fs[use_name](*this)) {
  use_name.clear();
 }

 // это была не последняя структура в цепочке использования
 return false;
}

Поля управляемой структуры инициализируются значением по умолчания, которое возвращает функция инициализации. Перед возвратом значения эта функция выполняет дополнительные действия, зависящие от типа поля.


для простых типов


регистрируется функция, которая выполняет установку значения поля по имени этого поля. Например, для структуры Person и поля age типа int


(такой код будет после подстановки макроса)


int age = [] {
 using value_type =
  std::conditional_t<std::is_same_v<bool, bool>, bool,
   std::conditional_t<std::is_same_v<std::string, bool>, std::string const &,
    std::conditional_t<std::is_floating_point_v<bool>, double, long long>>>;
     Fs_set_field<std::function<void(Person&, value_type)>>::add(
  "age",
  [] (Person & o, value_type value) {
   o.age = static_cast<bool>(value);
  });

 using USING_bool = bool;
 return USING_bool{};
}();

выражение


o.age = static_cast<bool>(value);

выполняет непосредственно установку значения для конкретного экземпляра структуры. В таком виде оно используется в gcc, для clang используется


bool Person::*p= &Person::age;
o.*p = static_cast<bool>(value);     

(различные варианты присутствуют, потому что clang не поддерживает первый вариант, а gcc падает по ICE на втором варианте)


для структур и массивов


регистрируются шесть функций, каждая из которых просто вызывает такую же функцию у структуры, в которой определяется поле. Например, для структуры Earth и поля president типа President


President president = [] {
 Fs_set<std::function<void(Earth&, std::string const &, bool)>>::add(
  "president",
  [] (Earth & o, std::string const & field_name, bool value) {
   o.president.set(field_name, value);
  });

 Fs_set<std::function<void(Earth&, std::string const &, double)>>::add(
  "president",
  [] (Earth & o, std::string const & field_name, double value) {
   o.president.set(field_name, value);
 });

 Fs_set<std::function<void(Earth&, std::string const &, long long)>>::add(
  "president",
  [] (Earth & o, std::string const & field_name, long long value) {
   o.president.set(field_name, value);
  });

 Fs_set<std::function<void(Earth&, std::string const &, std::string const &)>>::add(
  "president",
  [] (Earth & o, std::string const & field_name, std::string const & value) {
   o.president.set(field_name, value);
  });

 Fs_use<std::function<void(Earth&, std::string const &)>>::add(
  "president",
  [] (Earth & o, std::string const & name) {
   o.president.use(name);
  });

 Fs_release<std::function<bool(Earth&)>>::add(
  "president",
  [] (Earth & o) {
   return o.president.release();
  });

 using USING_President = President;
 return USING_President{};
}();

для clang, опять же, вместо


o.president.set(field_name, value);

используется


President Earth::*p = &Earth::president;
auto& pp = o.*p;
pp.set(field_name, value);

как все будет работать на примере следующего кода


#include <sstream>

#include "struct_mapping/struct_mapping.h"

#define MANAGED_STRUCT_NAME Author
BEGIN_MANAGED_STRUCT

MANAGED_FIELD(std::string, name)

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

#define MANAGED_STRUCT_NAME Book
BEGIN_MANAGED_STRUCT

MANAGED_FIELD(int, price)
MANAGED_FIELD_STRUCT(Author, author)
MANAGED_FIELD_ARRAY(std::string, chapters)

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

int main() {
 Book white_space;

 std::istringstream json_data(R"json(
  {
   "price": 273,
   "author": {
    "name": "bk192077"
   },
   "chapters": [
    "launch",
    "new horizons"
   ]
  }
 )json");

 struct_mapping::mapper::map_json_to_struct(white_space, json_data);
}

при компиляции


код


#define MANAGED_STRUCT_NAME Author
BEGIN_MANAGED_STRUCT

MANAGED_FIELD(std::string, name)

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

определяет структуру


struct Author {
 void set(std::string const &, bool) {...}
 void set(std::string const &, std::string &) {...}
 void set(std::string const &, long long) {...}
 void set(std::string const &, double) {...}
 void use(std::string const &) {...}
 bool release() {...}

 std::string name;
};

поле name будет инициализировано пустой строкой. До этой инициализации в экземпляр класса Fs_set_field будет добавлена функция установки значения для данного поля


Fs_set_field<std::function<void(Author &, std::string const &)>>::add(
 "name",
 [] (Author & o, std::string cont & value) {
  o.name = value;
 });

код


#define MANAGED_STRUCT_NAME Book
BEGIN_MANAGED_STRUCT

MANAGED_FIELD(int, price)
MANAGED_FIELD_STRUCT(Author, author)
MANAGED_FIELD_ARRAY(std::string, chapters)

END_MANAGED_STRUCT
#undef MANAGED_STRUCT_NAME

определяет структуру


struct Book {
 void set(std::string const &, bool) {...}
 void set(std::string const &, std::string &) {...}
 void set(std::string const &, long long) {...}
 void set(std::string const &, double) {...}
 void use(std::string const &) {...}
 bool release() {...}

 int price;
 Author author;
 ManagedArray<std::string> chapters;
};

поле price будет инициализировано нулем. До этой инициализации в экземпляр класса Fs_set_field будет добавлена функция установки значения для данного поля


Fs_set_field<std::function<void(Book &, long long)>>::add(
 "price",
 [] (Book & o, long long value) {
  o.price = static_cast<int>(value);
 });

поле author будет инициализировано значением по умолчанию Author. До этой инициализации в экземпляры классов Fs_ будут добавлены функции:


Fs_set<std::function<void(Book &, std::string const &, bool)>>::add(
 "author",
 [] (Book & o, std::string const & field_name, bool value) {
  o.author.set(field_name, value);
 });

Fs_set<std::function<void(Book &, std::string const &, double)>>::add(
 "author",
 [] (Book & o, std::string const & field_name, double value) {
  o.author.set(field_name, value);
 });

Fs_set<std::function<void(Book &, std::string const &, long long)>>::add(
 "author",
 [] (Book & o, std::string const & field_name, long long value) {
  o.author.set(field_name, value);
 });

Fs_set<std::function<void(Book &, std::string const &, std::string cont &)>>::add(
 "author",
 [] (Book & o, std::string const & field_name, std::string cont & value) {
  o.author.set(field_name, value);
 });

Fs_use<std::function<void(Book &, std::string const &)>>::add(
 "author",
 [] (Book & o, std::string const & name) {
  o.author.use(name);
 });

Fs_release<std::function<bool(Book &)>>::add(
 "author",
 [] (Book & o) {
  return o.author.release();
 });

действия с полем chapters будут аналогичны действиям с полем author


при выполнении


вызов map_json_to_struct создает экземпляр парсера и запускает процедуру парсинга, в процессе чего выполняются следующие действия (map_json_to_struct фактически просто транслирует вызовы управляемой структуре, поэтому ее действия не рассматриваются):


parser managed
start_struct("") это начало самой структуры Book, поэтому map_json_to_struct не транслирует этот вызов и он просто игнорируется
set_integral("price", 273) white_space.set("price", 273)
установка значения поля price у white_space: Fs_set_field<std::function<void(Book&, long long)>>::fs["price"](white_space, 273)
start_struct("author") после этого все вызовы white_space будет транслировать полю author: white_space.use("author")
set_string:("name", "bk192077") white_space.set("name", "bk192077")
трансляция к author (вызов метода set у author): Fs_set<std::function<void(Book, std::string const &, bool)>>::fs["author"](white_space, "name", "bk192077")
установка значения поля name у author: Fs_set_field<std::function<void(Author&, std::string const &)>>::fs["name"](author, "bk192077")
end_struct() white_space.release()
трансляция к author (вызов метода release у author): Fs_release<std::function<bool(Book&)>>::fs["author"](white_space))
после этого вызовы к white_space не будут больше транслироваться к author
start_array("chapters") после этого все вызовы white_space будет транслировать полю chapters: white_space.use("chapters")
set_string:("", "launch") white_space.set("", "launch")
трансляция к chapters (вызов метода set у chapters): Fs_set<std::function<void(Book, std::string const &, bool)>>::fs["chapters"](white_space, "", "launch")
добавление в массив элемента "launch"
set_string:("", "new horizons") white_space.set("", "new horizons")
трансляция к chapters (вызов метода set у chapters): Fs_set<std::function<void(Book, std::string const &, bool)>>::fs["chapters"](white_space, "", "new horizons")
добавление в массив элемента "new horizons"
end_array() white_space.release()
трансляция к chapters (вызов метода release у chapters): Fs_release<std::function<bool(Book&)>>::fs["chapters"](white_space))
после этого вызовы к white_space не будут больше транслироваться к chapters
end_struct("") это конец самой структуры Book, поэтому map_json_to_struct не транслирует этот вызов и он просто игнорируется

В итоге


  • желаемая простота использования. В основном все сводится к определению структуры с применением набора макросов (хотя можно и вручную все прописать)
  • работает медленно, но для основного применения в качестве загрузки конфигурации и начального состояния приложения подходит (скорость на этом этапе не важна)
  • требуется компиляция с -std=c++17 В основном для:
    • if constexpr
    • static inline

Библиотека доступна на GitHub

Теги:с++json
Хабы: Программирование C++
+12
5,9k 49
Комментарии 25

Похожие публикации

C++-программист
от 80 000 ₽Singularis LabВолгоградМожно удаленно
C++-программист
от 60 000 ₽Singularis LabВолгоградМожно удаленно
C++ разработчик
от 1 500 $ToqomoСанкт-ПетербургМожно удаленно
Ведущий инженер-программист С++
от 108 000 ₽Алмаз – АнтейМосква
C++-программист (техлид проекта)
от 100 000 ₽Singularis LabВолгоград

Лучшие публикации за сутки