- PVSM.RU - https://www.pvsm.ru -

Как написать рефлексию для C++

Как написать рефлексию для C++ - 1

C++ поистине противоречивый язык. Старый добрый С существует аж с 1972 года, С++ появился в 1985 и сохранил с ним обратную совместимость. За это время его не раз хоронили, сперва Java, теперь его потихоньку продолжают хоронить Go и Rust. Все его недостатки пережеваны множество раз. Если вы пришли в мир С++ из других ООП языков, то здесь вы не найдете:

  • Внятного стектрейса если где-то стрельнет исключение или SEGFAULT

  • Внятных сообщений об ошибках в некоторых случаях(в большинстве)

  • Естественно здесь нет сборки мусора, все придется делать руками

  • Чего-то стандартного, будь то система сборки, пакетный менеджер, решение для тестирования или даже компилятор.

  • И конечно же рефлексии

Им действительно тяжело пользоваться, особенно в крупных проектах, но он предоставляет большие возможности и пока не собирается на покой. На нем пишут игровые движки, софт для embedded систем, его используют Яндекс, VK, Сбер, множество финтех, крипто и блокчейн стартапов. Все потому что у С++ вместе с тем хватает и достоинств:

  • Производительность, из-за отсутствия сборки мусора и возможности низкоуровневых оптимизаций.

  • Умопомрачительные шаблоны и сопутствующая магия

  • Код, выполняемый во время компиляции

  • Богатая стандартная библиотека и Boost [1]

  • Малый размер скомпилированного файла

  • Поддержка всех возможных архитектур и операционных систем

Кроме того, за долгую жизнь языка написано огромное количество фреймворков, библиотек а также множество книг и неисчислимое количество статей. В целом писать на С++ очень интересно, но надо быть готовым к тому, что это полуфабрикат, который нужно уметь готовить. 

Проблема

Современная разработка и интернет неразрывно связаны в большинстве случаев. Сейчас любой утюг может передавать туда-сюда данные по REST в каком-нибудь JSON и нам, как разработчикам, необходимо как-то превращать их в конструкции языка и работать с ними.

Чтобы было проще думать о проблеме, представим, что мы хотим отправлять данные с датчика температуры/влажности и, соответственно, получать их на стороне сервера. Наши данные имеют вид:

struct TempHumData {
  string sensor_name;
  uint sensor_id;
  string location;
  uint update_interval_ms;
 
  struct Value {
    int temperature;
    uint humidity;
  };
 
  Value value;
}

Обычно, языки программирования позволяют работать с JSON как с DOM т.е. древовидной структурой данных описывающей некий объект. Свойства объекта могут быть числом, строкой или другим объектом. В С++ других вариантов нет:

#include "nlohmann/json.hpp"
 
nlohmann::json json;
 
json["sensor_name"] = "living_temp_hum";
json["sensor_id"] = 47589431;
json["location"] = "living_room";
json["update_interval_ms"] = 1000;
 
nlohmann::json nested_val;
 
nested_val["temperature"] = 24.3;
nested_val["humidity"] = 48;
 
json["value"] = nested_val;

К счастью есть возможность создать объект через парсинг JSON строки.

auto json = nlohmann::json::parse(json_str);

И где-то в другом месте проекта можно получить из него данные:

auto sensor = json["sensor_name"].get<std::string>();

Чем больше полей в объекте и чем шире он используется, тем хуже будут последствия. Любые более-менее серьезные изменения становятся болезненными и рутинными:

  • Название полей("sensor_name") это просто текст, поэтому и искать его придется как текст и редактировать тоже как текст. Никакого умного переименования в IDE.

  • Ошибки в имени никак не скажутся на компиляции, вместо этого в рантайме получим дефолтное значение, что не всегда очевидно.

  • Легко неправильно преобразовать тип - float в int или int в uint

И конечно же, приложение будет работать некорректно, а узнаете вы об этом не сразу, возможно в продакшене.

Есть вариант вручную присвоить полям структуры значения из DOM в отдельном файле:

TempHumData deserialize(const nlohmann::json& json) {
  TempHumData result;
 
  result.sensor_name        = json["sensor_name"].get<std::string>();
  result.sensor_id          = json["sensor_id"].get<uint>();
  result.location           = json["location"].get<std::string>();
  result.update_interval_ms = json["update_interval_ms"].get<uint>();
  result.value.temperature  = json["value.temperature"].get<int>();
  result.value.humidity     = json["value.humidity"].get<uint>();
 
  return result;
}

Тогда мы сможем пользоваться структурой. Ошибки будут в одном месте, но поможет это не сильно. Представьте, что будет, если количество полей устремится за 100+ или понадобится парсить множество разных JSON, полученных через REST API или из базы данных. Придется писать сотни строк кода, часто нажимать Ctrl+C, Ctrl+V, и человеческий фактор обязательно даст о себе знать. Кроме того, это придется проделывать каждый раз когда что-то меняется в объекте. В таком случае, ручной маппинг в структуру приносит больше боли чем пользы. 

Если мы используем другой язык программирования, можно сериализовать сам объект непосредственно и, соответственно, десериализовать JSON в объект.

Код на Go, имеющий такое поведение, выглядит следующим образом:

import “encoding/json"
 
type TempHumValue struct {
 Temperature float32 `json:"temperature"`
 Humidity    uint    `json:"humidity"`
}
 
type TempHumData struct {
 SensorName       string       `json:"sensor_name"`
 SensorId         uint         `json:"sensor_if"`
 Location         string       `json:"location"`
 UpdateIntervalMs uint         `json:"update_interval_ms"`
 Value            TempHumValue `json:"value"`
}
 
 
// somewhere
 
data := TempHumData{/* some data */}
 
bytes, _ := json.Marshal(data)
json_str := string(bytes)

В C# подобным функционалом обладает Newtonsoft Json [2], а в Java Jackson2 ObjectMapper [3]

В таком случае код парсинга и преобразования в структуру уже написан за нас и скрыт за интерфейсом, тип значения определяется автоматически, а любые изменения объекта остаются только в одном месте - в файле с определением структуры. Сам исходный код становится для нас своего рода контрактом. Кроме того, JSON либо корректно распарсится весь целиком либо не распарсится вовсе.

Все это становится возможным благодаря рефлексии(reflection, отражение) - способности программы понимать как именно она была написана. Как называются объекты, какого они типа, какие поля у них есть и сколько их, приватные они или публичные и т.д. Все это хранится в каком-то месте собранной программы и имеется логика, позволяющая эту информацию запрашивать.

кстати

Рефлексия полезна не только для сериализации/десериализации, но и для вызова методов по их имени, например, по наступлению событий в игровых движках или для реализации RPC. Но мы не будем реализовывать это в данной статье т.к. на самом деле решаем конкретную проблему, а рефлексия это только способ ее решения.

Одной из основных идей С++ является: “Мы не платим за то, что не используем”. И отсутствие такого механизма как рефлексия хорошо укладывается в рамки этой идеи. Примерный код на ассемблере, получаемый после компиляции Hello World:

section .data
  msg  db   'Hello world!'
  len  equ  $-msg
section .text
  mov rax, 1   ; set write as syscall
  mov rdi, 1   ; stdout file descriptor
  mov rsi, msg ; source buffer
  mov rdx, len ; number of bytes
  syscall      ; call write

Мы не храним информацию об исходном коде в привычном для программиста виде. Статические данные(секция .data) и набор инструкций(секция .text) просто упаковываются в бинарный файл. Тем самым, минимизируется размер файла и не тратится время на лишнюю инициализацию объектов в динамической памяти. В конце концов классы, функции, переменные - все это высокоуровневые абстракции нужные человеку, а не процессору.

Настало время рассказать немного о Rust. У него очень много общего с С++. Он построен на llvm [4](инструментарий компилятора С++), у него нет сборщика мусора, и он так же не поддерживает рефлексию. Но тем не менее у него есть очень классный serde [5], который не уступает решениям из других языков.

use serde::{Deserialize, Serialize};
 
#[derive(Serialize, Deserialize)]
struct TempHumValue {
   temperature: f32,
   humidity: u32,
}
 
#[derive(Serialize, Deserialize)]
struct TempHumData {
   sensor_name: String,
   sensor_id: u32,
   location: String,
   update_interval_ms: u32,
   value: TempHumValue,
}
 
// somewhere
 
let data = TempHumData {/* some data */};
 
let json_str = serde_json::to_string(&data).unwrap());

Секрет в данном случае прост, но не совсем очевиден. Rust имеет мощный механизм макросов, благодаря которому перед компиляцией генерируется [6] код, содержащий логику сериализации всей структуры поле за полем. Почти как в случае с ручным маппингом, только код пишет за нас компилятор.

Мы многое сделаем похожим на Rust и serde, но при этом немного отделим мух от котлет и разделим сериализацию и рефлексию. При всем при этом ни разу не заплатим за то, что не будем использовать.

Решение

Прежде всего надо определиться с принципами работы нашего решения. Если коротко, пошло и без интриги, то нам придется:

  • Написать библиотеку рефлексии которая позволит анализировать объекты, копировать их, создавать новые и т.д.

  • Добавить в нее поддержку стандартных типов: 

    • int, float и другие примитивы

    • строки

    • массивы

    • стандартные контейнеры, такие как std::vector и т.д.

  • Так же как в serde придется анализировать исходный код и генерировать новый, чтобы добавить поддержку новых типов - пользовательских enum(class), struct и class.

  • В конце концов написать сериализацию/десериализацию для нужных форматов.

Библиотека

Первая цель которой нам надо добиться - абстрагироваться от конкретного типа. Это довольно важный для понимания момент и на нем следует остановиться. Интуитивно, я бы хотел написать примерно такой код:

template <typename T>
void serialize_recursive(const T* obj) {
  std::vector<???*> fields = reflection::get_fields_of<T>(obj);
 
  for (auto&& one_field : fields) {
    serialize_recursive(one_field);      
  }
}

template <>
void serialize_recursive<int>(const int* obj) {
	// serealize int
}

template <>
void serialize_recursive<bool>(const bool* obj) {
  // serealize bool
}

Мне бы хотелось чтобы в fields хранились указатели разных типов на поля объекта, но это невозможно из-за особенностей языка. Компилятор просто не знает как физически хранить такие данные. Он так же не может знать какие именно типы могут там храниться, чтобы корректно вывести тип one_field, сгенерировать код для всех <T> и рекурсивно вызывать функцию. Сейчас мы работаем с одним объектом, через секунду с другим и у всех разное количество полей и их тип.

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

Первая сущность которая нам понадобится - Var [7] . Как ясно из названия это нечто, что представляет из себя переменную. Var хранит в себе:

  • указатель с типом void* на данные нашей переменной

  • id типа переменной

  • признак константная переменная или нет

У Var есть шаблонный конструктор, который принимает указатель произвольного типа, вычисляет id и стирает тип указателя преобразуя его к void*.

Получение id типа один из ключевых моментов. Монотонно возрастающий id дает возможность построить таблицу с указателями на функции, где id выполняет роль индекса и позволяет быстро вызывать нужную функцию. Это основная идея работы всей библиотеки рефлексии. Имея id типа и void* на данные мы можем вызвать либо:

static void copy(void* to, const void* from) {
  *static_cast<int*>(to) = *static_cast<const int*>(from);
}

либо:

static void copy(void* to, const void* from) {
  *static_cast<float*>(to) = *static_cast<const float*>(from);
}

Так, мы можем копировать переменные, создавать новые экземпляры и т.д. Нужно только добавить в таблицу указатель на функцию для конкретного действия.

кстати

В случае, если необходимо создать новый объект и вернуть его из функции, к сожалению, не обойтись без динамического выделения памяти. Компилятор должен знать тип(размер) объекта, если память аллоцируется на стеке. Следовательно, память придется выделять в куче, а возвращаемый тип сделать универсальным т.е. void* или Var.

Стандартный для C++ механизм получения id типа  typeid(T).hash_code() не даст монотонно возрастающей последовательности поэтому нам не подойдет.

Придется изобрести свой TypeId [8] который будет содержать единственный int в качестве данных и дополнительную логику. По умолчанию он инициализируется значением 0 - неизвестный тип, остальные значения задаются через специализации. Например:

TypeId TypeId::get(int* /*unused*/) {
 static TypeId id(TheGreatTable::record(Actions(
                &IntActions::reflect,
                &IntActions::call_new,
                &IntActions::call_delete,
                &IntActions::copy)));
 return id;
}

Я оставил только необходимое для понимания, оригинал [9] в репозитории.

Здесь есть довольно хитрый момент. Специализация TypeId::get(T* ptr) использует приватный конструктор TypeId, который принимает число - собственно id. Это число мы получаем вызовом TheGreatTable::record(). Оно остается в статической переменной, следовательно будет инициализировано только один раз, дальше просто возвращается.

Правильно написанный шаблонный код уменьшит количество boiler plate, а статическая инициализация позволит нам не задумываться у какого типа какой id, все будет происходить автоматически без нашего участия.

TheGreatTable [10] это еще одна ключевая сущность библиотеки. Та самая таблица с указателями на функции. Запись в нее возможна только через метод record(), который регистрирует указатели и возвращает индекс в таблице т.е. id типа. В примере выше, в нее записываются указатели на четыре функции.

Таким образом, мы можем быстро и безболезненно определить тип в рантайме и вызывать соответствующий код, различные проверки, которые обычно делает компилятор, тоже придется делать в рантайме, например [11]:

Expected<None> reflection::copy(Var to, Var from) {
  if (to.is_const()) {
    return Error("Cannot assign to const value");
  }
  if (to.type() != from.type()) {
    return Error(format("Cannot copy {} to {}", type_name(from.type()), type_name(to.type())));
  }
  TheGreatTable::data()[to.type().number()].copy(to.raw_mut(), from.raw());
  return None();
}

Для того чтобы хранить о типе всю необходимую информацию и иметь универсальную логику работы с ним, нам понадобится еще одна сущность. 

TypeInfo [12] представляет собой sum type на основе std::variant [13] с чуть более объектно ориентированным интерфейсом. Вызовом метода match() можно определить, что именно представляет из себя тип:

info.match([](Bool& b) { std::cout << “booln”; },
           [](Integer& i) { std::cout << “integern”; },
           [](Floating& f) { std::cout << “floatingn”; },
           [](String& s) { std::cout << “stringn”; },
           [](Enum& e) { std::cout << “enumn”; },
           [](Object& o) { std::cout << “objectn”; },
           [](Array& a) { std::cout << “arrayn”; },
           [](Sequence& s) { std::cout << “sequencen”; },
           [](Map& m) { std::cout << “mapn”; },
           [](auto&&) { std::cout << “something elsen”; });

Любой тип может представлять собой один из следующих вариантов:

  • Bool - один единственный тип bool

  • Integer - все целые типы, включая char

  • Floating - числа с плавающей запятой: float и double

  • String - строковые типы включая std::string_view

  • Enum - разные enum и enum class

  • Object - структуры и классы, позволяет искать поле по имени и получить список всех полей.

  • Array - классические массивы в стиле С

  • Sequence - стандартные контейнеры с одним шаблонным параметром 

  • Map - ассоциативные контейнеры с двумя шаблонными параметрами

Для того чтобы абстрагироваться от конкретных типов применяется type erasure. Шаблонный код для разных типов(int32_t, uint64_t, char) скрыт за общим интерфейсом(Iinteger [14]) и работает с Var и другими универсальными сущностями.

Вся работа начинается с вызова основной функции рефлексии er::reflection::reflect(), которая возвращает [15] TypeInfo. Дальше мы имеем возможность рекурсивно разобрать наш тип и понять как он устроен и какие данные хранит.

Мне не хочется превращать статью в документацию. Поэтому код для поддержки стандартных типов оставлю по ссылке [16]. Если какой-то из них не будет использован в приложении, то статическая инициализация не сгенерирует TypeId, не добавит указатели на функции в TheGreatTable, а компилятор вырежет ненужный код, и мы не заплатим за то, что не будем использовать.

Теперь мы разобрались с основными принципами работы библиотеки и нам надо как-то добавить поддержку пользовательских структур и классов.

Генератор

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

кстати

Существующие решения для рефлексии в C++ мне не нравятся как раз тем, что заставляют писать кучу кода используя уродливые макросы. Это приходится делать потому что информацию нужно как-то добавить в бинарный файл с программой и добавлять ее приходится руками.

Мы пойдем иным путем. Мы воспользуемся API компилятора чтобы автоматизировать сборку необходимой информации. К счастью в 2007 году вышла первая версия Clang и LLVM, с тех пор появилось множество полезных утилит анализирующих исходный код, например, clang-format, clang-tidy и объединяющий их clangd. Используя те же принципы мы напишем свою утилиту для анализа исходного кода. Сами исходники при этом можно будет компилировать чем угодно, хоть gcc, хоть MSVC(но, как всегда, с нюансами).

Clang предоставляет libTooling - набор библиотек для анализа исходного кода. С его помощью мы можем анализировать исходный код точно так же как это делает компилятор т.е. через Abstract Syntax Tree [17]. Это даст нам много бонусов, по сравнению с ручным анализом исходного кода. AST содержит информацию из множества файлов, следовательно, предоставляет больше информации, позволяет понять в каком пространстве имен находится тот или иной объект, легко отличить объявление(declaration) от определения(definition) и т.д.

Кроме доступа к AST, у нас будет доступ к препроцессору, он позволит в качестве атрибутов применить пустые макросы:

#define ER_REFLECT(...) // expands to nothing
 
ER_REFLECT()
struct TempHumData {
  // struct fields
}

Взаимодействие с libTooling, в основном, происходит посредством обратных вызовов. Например, когда препроцессор разворачивает макрос или во время обхода AST встречается определение класса. Внутри них мы можем анализировать поддеревья AST и получать имена полей, типов, модификаторы доступа и т.д. Собранную информацию следует сохранить в какой-нибудь промежуточной структуре данных. Как это происходит на самом деле можно посмотреть в файле parser_cpp.h [18].

Так же нам надо как-то генерировать код, основываясь на собранной информации. Для этого отлично подходят движки шаблонов, такие как go template [19], mustache [20], jinja [21] и др. Мы напишем руками всего несколько шаблонов, по которым будем генерировать сотни новых файлов с исходным кодом. В этом проекте я решил использовать inja [22], своего рода C++ порт jinja для Python. 

Упрощенный файл шаблона для объектов выглядит следующим образом:

template <>
struct TypeActions<{{name}}> {
  static TypeInfo reflect(void* value) {
    auto* p = static_cast<{{name}}*>(value);
 
    static std::map<std::string_view, FieldDesc> map {
      {% for item in fields_static -%}
      {"{{item.alias}}", FieldDesc::create_static(Var(&{{name}}::{{item.name}}), {{item.access}})},
      {% endfor %}
      {% for item in fields -%}
      {"{{item.alias}}", FieldDesc::create_member(value, Var(&p->{{item.name}}), {{item.access}})},
      {% endfor %}
    };
 
    return Object(Var(p), &map);
 
  }
};
 
template <>
TypeId TypeId::get({{name}}* /*unused*/) {
  static TypeId id(TheGreatTable::record(Actions(&TypeActions<{{name}}>::reflect,
    &CommonActions<{{name}}>::call_new,
    &CommonActions<{{name}}>::call_delete,
    &CommonActions<{{name}}>::copy)));
 
  return id;
}

Оригинал находится по ссылке [23]

TypeActions<T> это просто обертка, чтобы не засорять код и не насиловать автодополнение в IDE сгенерированными именами классов и функций.

Вместо {{name}} будет вставлено имя класса или структуры. 

При первом вызове reflect() в два этапа заполняется статическая std::map где ключом является имя поля, а значением его дескриптор. Позже, благодаря ему, можно будет получить FieldInfo [24], который хранит в себе Var и модификатор доступа - public, private и т.д. На первом этапе регистрируются только статические поля. Это позволит обеспечить к ним доступ даже без экземпляра класса.

ClassWithStaticFields* ptr = nullptr;
auto info = reflection::reflect(ptr);

На втором этапе регистрируются указатели на все остальные поля, в том числе и приватные. Благодаря этому можно гибко контролировать доступ к ним. Десериализовать данные только в публичные поля, а приватные только читать и печатать в консоль. 

Далее указатель на std::map помещается в Оbject, который упаковывается в TypeInfo и возвращается из функции.

В специализации TypeId::get указатели на функции регистрируются в TheGreatTable

Сгенерированный код для всех пользовательских типов будет находиться в reflection.h и reflection.cpp, следовательно, скомпилируется в отдельный объектный файл. Такая организация упростит сборку проекта, но об этом чуть позже. Для удобства все настройки для генератора, в том числе путь к анализируемым и генерируемым файлам описываются в YAML файле [25].

Сериализация

Код сериализаторов для JSON, YAML и массива байт можно найти в репозитории [26]. Бинарная сериализация как и protobuf оптимизирует размер данных на лету.

Производительность сериализации примерно такая же как у rapid_json . Для десериализации я написал парсеры JSON и YAML с использованием лексера. К сожалению я обычное быдло, а не гуру алгоритмов и оптимизаций, поэтому нативный парсер чуть быстрее nlohmann::json, но медленнее rapid_json . Тем не менее использование simdjson [27] в качестве парсера позволяет даже немного обойти rapid_json .

Бенчмарки [28] позволяют самостоятельно оценить производительность на разном железе.

Как написать рефлексию для C++ - 2

Собираем все вместе

На данный момент у нас есть:

  • Библиотека рефликсии и сериализации

  • Шаблоны, с помощью которых будет генерироваться код

  • Анализатор и генератор исходного кода в отдельном приложении

Все что нам осталось, это расставить атрибуты в исходном коде и настроить систему сборки так, чтобы перед шагом компиляции основного проекта, генерировался код для рефлексии новых типов. В CMake это можно сделать через add_custom_command:

set(SOURCES
   main.cpp
   ${CMAKE_CURRENT_SOURCE_DIR}/generated/reflection.cpp)
 
add_custom_command(
   OUTPUT
       ${CMAKE_CURRENT_SOURCE_DIR}/generated/reflection.cpp
   COMMAND er_gen -p -c ${CMAKE_CURRENT_SOURCE_DIR}/config.yaml
   DEPENDS
       data/temp_hum.h
   COMMENT "Generating reflection headers")
 
add_executable(${PROJECT_NAME} ${SOURCES})

К счастью весь сгенерированный исходный код находится в одном .h и одном .cpp файле поэтому достаточно включать reflection.h для доступа к API, a reflection.cpp добавить в список файлов с исходным кодом.

Если файлы в секции DEPENDS изменятся, кодогенератор запустится автоматически. Дальше остается только получать удовольствие от программирования на С++ и сериализовать объект одной строкой:

auto json_str = serialization::json::to_string(&obj).unwrap()

И в обратную сторону:

auto sensor_data = serialization::simd_json::from_string<TempHumData>(json_str).unwrap();

Более развернутый пример можно найти в репозитории [29] с проектом:

Итог

Такое решение позволяет получить максимально близкий к другим языкам опыт. Отличие заключается только в небольшом колдунстве над процессом сборки. Кроме того, его функционал легко расширить.

Проект протестирован и может быть использован в продакшене, но тем не менее многие вещи могут быть доделаны или улучшены. Если у вас будут какие-то идеи или предложения, я всегда буду рад любой помощи и, конечно же, звездам на гитхабе [30].

Статья получилась объемной, но некоторые темы не были раскрыты. Например, как устроен парсинг JSON или YAML, как устроена бинарная сериализация. Если вы хотите узнать что-то в следующей статье, пожалуйста, дайте знать, что именно.

Автор: Max Voloshin

Источник [31]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/373008

Ссылки в тексте:

[1] Boost: https://www.boost.org/

[2] Newtonsoft Json: https://www.newtonsoft.com/json/help/html/SerializingJSON.htm

[3] Jackson2 ObjectMapper: https://github.com/FasterXML/jackson-databind/

[4] llvm: https://llvm.org/

[5] serde: https://serde.rs/

[6] генерируется: https://www.joshmcguigan.com/blog/understanding-serde/

[7] Var: https://github.com/chocolacula/reflection_cpp/blob/main/library/include/er/variable/var.h

[8] TypeId: https://github.com/chocolacula/reflection_cpp/blob/main/library/include/er/type_id.h

[9] оригинал: https://github.com/chocolacula/reflection_cpp/blob/db40dac55e4fc190639f6728c8d92c59ebdae785/library/include/er/types/integer.h#L21

[10] TheGreatTable: https://github.com/chocolacula/reflection_cpp/blob/main/library/include/er/reflection/the_great_table.h

[11] например: https://github.com/chocolacula/reflection_cpp/blob/db40dac55e4fc190639f6728c8d92c59ebdae785/library/src/reflection/reflection.cpp#L58

[12] TypeInfo: https://github.com/chocolacula/reflection_cpp/blob/main/library/include/er/type_info/type_info.h

[13] std::variant: https://en.cppreference.com/w/cpp/utility/variant

[14] Iinteger: https://github.com/chocolacula/reflection_cpp/blob/main/library/include/er/type_info/variants/integer/iinteger.h

[15] возвращает: https://github.com/chocolacula/reflection_cpp/blob/db40dac55e4fc190639f6728c8d92c59ebdae785/library/src/serialization/json/json.cpp#L131

[16] ссылке: https://github.com/chocolacula/reflection_cpp/tree/main/library/include/er/types

[17] Abstract Syntax Tree: https://clang.llvm.org/docs/IntroductionToTheClangAST.html

[18] parser_cpp.h: https://github.com/chocolacula/reflection_cpp/blob/main/generator/parser_cpp.h

[19] go template: https://pkg.go.dev/text/template

[20] mustache: https://mustache.github.io/

[21] jinja: https://palletsprojects.com/p/jinja/

[22] inja: https://github.com/pantor/inja

[23] ссылке: https://github.com/chocolacula/reflection_cpp/blob/main/generator/templates/object.inja

[24] FieldInfo: https://github.com/chocolacula/reflection_cpp/blob/main/library/include/er/type_info/variants/object/field_info.h

[25] файле: https://github.com/chocolacula/reflection_cpp/blob/main/example/config.yaml

[26] репозитории: https://github.com/chocolacula/reflection_cpp/tree/main/library/src/serialization

[27] simdjson: https://github.com/simdjson/simdjson

[28] Бенчмарки: https://github.com/chocolacula/reflection_cpp/tree/main/benchmarks

[29] репозитории: https://github.com/chocolacula/reflection_cpp/blob/main/example/main.cpp

[30] гитхабе: https://github.com/chocolacula/reflection_cpp

[31] Источник: https://habr.com/ru/post/655645/?utm_source=habrahabr&utm_medium=rss&utm_campaign=655645