Comments 9
Здорово! Полезно! Спасибо!
Вы описываете механизм конвертации в JSON объектов с примитивными типами данных, но как быть со вложенными объектами? В вашем случае, если я конечно же правильно понял идею, придется на каждый класс писать свою реализацию своего виртуального оператора, что меня смущает. Моя идея заключается в единоразовом описании полей в Q_PROPERTY и последующей работой с пространством имен.
Насчет Enum и хардкода вектора я полностью с вами согласен, насчет вектора вообще ужасное решение.
А вот насчет выделения памяти надо разбираться, хранимые указатели на целевой QObject занимают всего по 8 байт (на x64), объект QMetaProperty 32 байта, действительно расход большой, 40 байт на хранитель, но эта память освобождается сразу же после разрушения фабрики, которая предоставила в пользование объекты keepers. (т.е. суммарно при сериализации объекта с 5ю полями, на время сериализации будет выделено 200 байт, не кисло). Стоит ли сериализация такого расхода, пусть и кратковременного — решать вам, вообще серализация довольно затратный по памяти процесс.
Как я сказал в конце — приходится чем-то жертвовать, если описывать реализацию оператора сериализации в классе, вы приобретаете в гибкости, но теряете в простоте использования. Более того, вам понадобится такой же оператор и для XML и для бинарного формата(который, я надеюсь, скоро появится в QSerializer), в итоге тут уже попахивает вынесением такой функциональности в отдельный класс (меня привлекает концепция миниатюрных классов, описанная в «Clean Code» Боба Мартина, и, на мой взгляд, стремление к простоте — вовсе не грех).
Я учту ваши замечания, считаю такой опыт полезным, поэтому спасибо за ваше время.
По поводу списков согласен, моё решение тоже костыль, наверное, бОльший в вашем случае. Я не придумал, как (де)сериализовать списки как обычный Q_PROPERTY без проверок на тип внутри (де)сериализатора (то же касается вложенных сложных объектов), особенно если это список наследников QObject и их надо пробрасывать в qml именно как список. Хочется чего-то, что не потребует изменения кода сериализатора для каждого нового типа. Обычно всё выливается в вид QList<QObject*> с неприятными кастами на каждом шагу (это всё для проброса в qml) либо в QQmlListProperty, где много лишнего кода в реализации (или макросы, но большие проекты компилируются медленнее из большого кол-во кода в header-е).
вашем случае, если я конечно же правильно понял идею, придется на каждый класс писать свою реализацию своего виртуального оператора, что меня смущает.
Да, правильно поняли, у вас эту проблему решают кастомный кипер.
Скажите, а при необходимости использования get/set из c++ в реальном коде вы используете MEMBER в Q_PROPERTY или какие-то макросы, которорые делают Q_PROPERTY/signal/getter/setter/{private,protected} поле? Честно говоря, с MEMBER из вашей статьи познакомился, интересно.
Я у себя использую макросы вида PROPERTY(type, name)
и PROPERTY_DEFAULT(type, name, default)
, которые разворачиваются во всё вышеописанное + type m_ ## property = default
во втором случае (что тоже неприятно радует временем компиляции, когда в проекте тысячи таких PROPERTY, зато избавляет от ошибок).
Ещё интересно, как вы возвращаете из getter-ов (если они есть) вложенные наследники QObject: как указатель, что, по-моему, плохая идея, либо как копию/ссылку на объект. Во втором случае в Qt есть два подхода: возвращение копии с последующим вызовом setter-а и возвращение ссылки, которую можно дергать иерархически и на любом уровне менять объект. По моему актуальный вопрос при работе с qml.
По поводу использования памяти у вас всё хорошо подытожено в заключении, остаётся только сказать, что есть примитивные классы (например, какой-нибудь vector3d), которых много и скаждым можно работать в qml. Тогда оверхед QObject-a в 16 байт выигрывает перед 40 байтами с дополнительными выделениями памяти.
Кстати, оба подхода позволяют работать не только с json. Нашёл очень удобным для себя оператор QDebug operator<<(QDebug d, const Object &o)
с проходом по полям, вдруг пригодится.
Остаётся только пожелать, чтобы в Qt6 завезли хорошую годную рефлексию на QObject-based property и их списки. А вашу библиотеку обязательно попробую.
Хранить по объекту на property попахивает оверхедом, достаточно итерироваться по property и писать сразу в json, keeper-ы не нужны. Так быстрее и память не занимает.
BaseClass::operator const QJsonObject() const
{
QJsonObject result;
// use BaseClass static offset, not virtual
for(int i = staticMetaObject.propertyOffset(); i < metaObject()->propertyCount(); i++)
{
auto key = metaObject()->property(i).name();
auto type = metaObject()->property(i).userType();
auto value = metaObject()->property(i).read(this);
if(value.isNull())
{
// qDebug() << .err..
continue;
}
auto converted = value.convert(type);
if(!converted)
{
// qDebug() << .err..
}
result[key] = QJsonValue::fromVariant(value);
}
return result;
}
Наследуясь от такого класса любой объект получает (виртуальный) оператор и можно писать QJsonObject json = myObject и регистрировать типы в массивах не надо. Остаётся только добавить отличие USER свойств и вложенных объектов. Хардкодить std::vector в регистрации тоже как-то некрасиво. Я себе сделал SerializableVector класс с похожей логикой, лучше ничего не придумал). QVariant(val), кстати, не работает с enum-ами (которые зарегистрированы в Q_ENUM/Q_ENUM_NS).
Можно посмотреть еще в сторону Q_GADGET, что б сериализуемые объекты не были такими тяжёлыми с багажом от QObject.
QSerializer: решение для простой сериализации JSON/XML