Pull to refresh

Сериализация в Qt через использование MetaObject

Reading time4 min
Views12K

Предыстория


Собственно для чего такое могло бы понадобиться? Ведь C++ и так предоставляет достаточно гибкие возможности при сериализации в поток. Однако у меня стояла задача максимально универсализировать процесс сериализации/десериализации для многократного использования в проектах.

Итак, было надо организовать как можно более гибкую систему (де)сериалиации в Qt, так чтобы можно было
  1. либо отнаследовавшись от базового класса и расширив его
  2. либо имея отдельный класс-сериализатор
иметь возможность одной командой отправить поток данные из объекта.

При этом каким-либо образом должна была быть обеспечена возможность указывать какие данные в объекте подлежат сериализации, а какие можно (и нужно) «проскипать». Аналогично должна была быть выполнена возможность при десериализации правильно установить данные и связанные с ними зависимые величины внутри объекта.

Решение



Решение было найдено в виде системы свойств (property). Qt имеет надстройки над C++ для обеспечения системы свойств. Свойства мы имеем право использвать лишь в классах отнаследовавшихся от QObject, имеющих тег Q_OBJECT. Назначение свойств происходит при использовании макроса такого вида при описании класса в хедере (пример):
Q_PROPERTY(int Test READ readTest WRITE setTest)

Как видно из примера можно назначить функцию чтения (readTest) и записи (setTest). Последнее обеспечивает при десериализации корректность установки.

Необходимые для сериализации параметры набираются в объекте в виде свойств, тем самым задавая список свойств нужных для обеспечения целостности данных класса.

Для обеспечения доступа к свойствам в общем виде (учитывая о том что базовый класс о классе который передал себя на сериализацию ничего знать и не может) получается чрез систему MetaObject (описывающий объект с его системой свойств и генерируемый для любого объекта) MetaProperty, которые обеспечивают описание и доступ к свойстам. В итоге получился такой код для (де)сериализации:
bool SerializedBase::Serialize(QDataStream *dataStream)
{
  if (dataStream == NULL)
    return false;
  for (int i = 1; i < this->metaObject()->propertyCount(); i++)
  {
     QMetaProperty prop = this->metaObject()->property(i);
     const char* propName = prop.name();
     *dataStream << (this->property(propName));
  }
  return true;
}

bool SerializedBase::DeSerialize(QDataStream *dataStream)
{
  if (dataStream == NULL)
    return false;
  for (int i = 1; i < this->metaObject()->propertyCount(); i++)
  {
     QMetaProperty prop = this->metaObject()->property(i);
     const char* propName = prop.name();
     QVariant v;
     *dataStream >> v;
     this->setProperty(propName, v);
  }
  return true;
}


* This source code was highlighted with Source Code Highlighter.

В итоге получился базовый класс, отнаследовавшись от которого мы можем в 1 команду (при наличии любого созданного потока данных) сериализовать/десериализовать объект.

Код для отдельностоящего класса должен быть практически идентичным, разве что доп. проверки для того чтобы понять объект перед нами или обычная структура данных).

Дополнительные сведения и модификации



Объекты в виде свойств и их сериализация

Директива Q_DISABLE_COPY в коде класса QObject не дает возможности назначать свойствам напрямую значения-объекты. Для того чтобы обеспечить необходимую гибкость, нужно совершить 3 действия:
  1. Создать конструктор-копию. Допустим если объект класс ААА, то конструктор должен выглядеть так(в хэдере): AAA(AAA* copy). В нем собственно определяются действия при копировании объекта.
  2. Зарегистрировать метакласс в хэдере класса. Делается так: Q_DECRARE_METATYPE(AAA);
  3. При использовании свойства чтобы не «ограсти» проблем (обычно все же они определяются на уровне компилятора, так что не пропустите), необходимо где-нибудь в приложении сделать для класса регистрацию через команду qRegisterMetaType("AAA")
    Необходимо переназначить для используемых классов стриминг, то есть указать как их засылать в поток. Делается через команду qRegisterMetaTypeStreamOperators ( const char * typeName ). Описание функций которые должны быть реализованы в классе:

    QDataStream &operator<<(QDataStream &out, const MyClass &myObj);
    QDataStream &operator>>(QDataStream &in, MyClass &myObj);


После всех этих действий мы сможем оперировать с свойствами-объектами, не только значениями стандартных типов, и не пользоваться ссылками на данные типы типы (см. недостатки).

Использование динамически создаваемых инстанций

Система свойств Qt это поддерживает, правда опять же не «прямо», надо совершать телодвижения. Класс (базовый) должен соответствовать требованиям в вышенаходящемся списке (ну разве пункт 4 уже не обязателен), плюс еще (спасибо за подсказку fuCtor):
1. Используемый конструктор должен иметь макрос Q_INVOKABLE в своем описании
2. Использовать нужно команду QMetaObject::newInstance(), прицепившись к имени класса которое может передаваться вместе с объектом (или его идентификатор) QMetaObject. Если способа MetaObject готовый получить нет, придётся довольствоваться дефолтным конструктором (который тем не менее должен удовлетворять тем же требованиям, и пользоваться QMetaType::construct, до этого получив по имени Id зарегистрированного типа через int QMetaType::type ( const char * typeName ), а по Id (не забыв проверить зарегистрирован ли в системе тип сравнив с 0) уже сам MetaType.

Преимущества и недостатки


Вкратце обозначу преимущества данного метода:
  • Быстрая адаптация под новые классы — напиши свойства, функции для их получения и установки, отнаследуйся — и вуаля, сериализация работает
  • Поддерживается система как статических, так и динамических свойств. Если в систме клиент-сервер на том конце при десериализации объект не поддерживает свойства с опред. именем — оно создается динамически, и его можно отследить всё равно (правда при этом сеттер/геттер не появится из ниоткуда. Отданако и данные мы не потеряем)
  • Оно работает :)


А вот недостатки, выявленные на текущий момент:
  • Невозможность корректной передачи свойств-ссылок. Адреса-то передаются, а вот данные за ссылками-нет. Физически свойства-указателей делать нам никто не запрещает, однако сериализация с ними корректно работать не будет
  • Нестандартные типы свойств надо регистрировать с помощью Q_DECLARE_METATYPE для того, чтобы была возможность ими оперироваться через QVariant
  • Если пытаться использовать сами объекты в виже свойств, требуется писать много дополнительного кода в классе — конструкторы, макросы и тп. Правда в итоге это выливается в большую универсальность.
Tags:
Hubs:
+31
Comments12

Articles